> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prisme.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Building a Data Classification Agent

> Learn how to create an AI-powered document classification system with Prisme.ai

This tutorial guides you through building an intelligent document classification system using Prisme.ai. You'll create a solution that automatically categorizes uploaded documents into types such as invoices, CVs, quotes, contracts, and more using the power of generative AI.

## What You'll Build

A complete document management system with:

* A user-friendly document upload interface
* Automatic AI-based document classification
* A searchable and filterable document repository
* Document management capabilities (view, categorize, delete)

<Note>
  This solution combines a classification agent built in Agent Creator with the Collection app for document storage, creating a powerful yet simple document management system.
</Note>

## Prerequisites

Before starting this tutorial, make sure you have:

* An active Prisme.ai account
* The Agent Factory App installed in your workspace
* An **Agent Factory API key** generated in Agent Creator (open any agent → **Settings** → **API Keys**) and pasted into the Agent Factory App's configuration field `apiKey` after installing it. This is required when the form page is public — without it, `Agents.sendMessage` returns `401 UNAUTHORIZED` for unauthenticated form submissions.
* The Collection app installed in your workspace
* A classification agent created in [Agent Creator](/products/agent-factory/creating-agents) with system instructions like: *Classify the document within these categories: invoice, CV, quote, contract, others. Provide your categorization based on the content. Answer with the category only.*
* Basic familiarity with Prisme.ai Builder

## Step 1: Creating Your Document Management Workspace

Let's start by setting up a dedicated workspace for our document management system:

<Steps>
  <Step title="Access Builder">
    Log in to your Prisme.ai account and navigate to **Builder**.
  </Step>

  <Step title="Create a New Workspace">
    Click **+ Create new workspace**, choose **From Scratch**, name your workspace "Document Management System", add a short description, and click **Create Workspace**.
  </Step>
</Steps>

<img src="https://mintcdn.com/prismeai/K_v8yhAp7bkcmKW-/images/tutorials/builder-create-wizard-step2.png?fit=max&auto=format&n=K_v8yhAp7bkcmKW-&q=85&s=0f1200a9acbe99a419e71ae23e7d5c06" alt="Builder Create Workspace wizard step 2: name and description" width="1440" height="900" data-path="images/tutorials/builder-create-wizard-step2.png" />

*The screenshot above shows the wizard with the example name "Tutorial — Webhook Builder". For this tutorial, fill in "Document Management System" instead.*

## Step 2: Building the Document Upload Interface

Now, let's create the upload interface. In v27 Builder, the frontend of a workspace is a single editable page called `index` — a React + Vite + Tailwind + Radix SPA, not a block canvas.

<Note>
  If you came from the legacy Pages model, the block YAML (`slug: Form`, `slug: DataTable`) is gone. Pages are React SPA source trees; the platform compiles and previews them inside Builder. The page talks to automations through HTTP webhooks and WebSocket events instead of `onSubmit` / `updateOn` block events.
</Note>

<Steps>
  <Step title="Open Pages">
    In your workspace sidebar, select **Pages**. The editable `index` entry is your workspace's React app.
  </Step>

  <Step title="Switch to Code mode">
    Use the page toolbar to switch to **Code**. If the workspace has no source yet, initialize the React/Vite/Tailwind/Radix starter template — Builder will scaffold `src/App.tsx`, `src/main.tsx`, `src/styles/globals.css`, `index.html`, `package.json`, and `vite.config.ts`.
  </Step>

  <Step title="Edit src/App.tsx">
    Open `src/App.tsx` and replace its contents with the component below. It renders an upload form (file input + description textarea), lists uploaded documents in a table, and stays in sync with the backend through `documents.updated` events.

    The component receives this prop interface from the platform:

    ```ts theme={null}
    interface AppProps {
      sdk: SDK
      user: unknown
      workspace: { id: string; slug: string; name: string }
      backends?: Record<string, { slug: string }>
    }
    ```

    A complete, working example:

    ```tsx theme={null}
    import { useEffect, useState } from 'react'

    interface AppProps {
      sdk: any
      user: unknown
      workspace: { id: string; slug: string; name: string }
    }

    interface DocumentRow {
      _id: string
      description: string
      category?: string
      attachment?: string
    }

    export default function App({ sdk, workspace }: AppProps) {
      const [description, setDescription] = useState('')
      const [file, setFile] = useState<File | null>(null)
      const [documents, setDocuments] = useState<DocumentRow[]>([])
      const [submitting, setSubmitting] = useState(false)
      const [error, setError] = useState<string | null>(null)

      const baseUrl = `${sdk.host}/workspaces/slug:${workspace.slug}/webhooks`
      const headers = {
        'Content-Type': 'application/json',
        ...(sdk.token ? { Authorization: `Bearer ${sdk.token}` } : {}),
        ...(sdk._csrfToken ? { 'x-prismeai-csrf-token': sdk._csrfToken } : {}),
      }

      async function loadDocuments() {
        const res = await fetch(`${baseUrl}/list-documents`, { method: 'POST', headers, body: '{}' })
        const data = await res.json()
        setDocuments(data.documents ?? [])
      }

      async function handleSubmit(e: React.FormEvent) {
        e.preventDefault()
        setSubmitting(true)
        setError(null)
        try {
          // Demo: send the file name as `attachment`. For real uploads, POST the
          // file to `${sdk.host}/v2/files` first and pass back the returned URL,
          // or read the file as base64 and send it inline.
          const res = await fetch(`${baseUrl}/submit-document`, {
            method: 'POST',
            headers,
            body: JSON.stringify({
              description,
              attachment: file?.name ?? null,
            }),
          })
          const data = await res.json()
          if (!data.success) throw new Error('Submission failed')
          setDescription('')
          setFile(null)
        } catch (err: any) {
          setError(err.message ?? 'Unknown error')
        } finally {
          setSubmitting(false)
        }
      }

      async function handleDelete(id: string) {
        await fetch(`${baseUrl}/delete-document`, {
          method: 'POST',
          headers,
          body: JSON.stringify({ id }),
        })
      }

      useEffect(() => {
        loadDocuments()
        let cancelled = false
        let cleanup: (() => void) | undefined

        ;(async () => {
          const events = await sdk.streamEvents(workspace.id, { 'source.sessionId': true })
          if (cancelled) return
          const handler = () => loadDocuments()
          events.on('documents.updated', handler)
          cleanup = () => events.off?.('documents.updated', handler)
        })()

        return () => {
          cancelled = true
          cleanup?.()
        }
      }, [workspace.id])

      return (
        <div className="mx-auto max-w-3xl p-6 space-y-8">
          <header>
            <h1 className="text-2xl font-semibold">Document Management System</h1>
            <p className="text-sm text-gray-500">Upload a document and let the agent classify it.</p>
          </header>

          <form onSubmit={handleSubmit} className="space-y-4 rounded-xl border p-6">
            <div>
              <label className="block text-sm font-medium mb-1">Document</label>
              <input
                type="file"
                accept=".pdf"
                onChange={(e) => setFile(e.target.files?.[0] ?? null)}
                className="block w-full text-sm"
              />
            </div>
            <div>
              <label className="block text-sm font-medium mb-1">Description</label>
              <textarea
                value={description}
                onChange={(e) => setDescription(e.target.value)}
                placeholder="Describe the document here."
                className="block w-full rounded-md border px-3 py-2 text-sm"
                rows={4}
                required
              />
            </div>
            {error && <p className="text-sm text-red-600">{error}</p>}
            <button
              type="submit"
              disabled={submitting}
              className="rounded-md bg-black px-4 py-2 text-sm font-medium text-white disabled:opacity-50"
            >
              {submitting ? 'Submitting...' : 'Submit'}
            </button>
          </form>

          <section>
            <h2 className="text-lg font-semibold mb-3">Uploaded Documents</h2>
            <table className="w-full text-sm border">
              <thead className="bg-gray-50">
                <tr>
                  <th className="text-left p-2">ID</th>
                  <th className="text-left p-2">Description</th>
                  <th className="text-left p-2">Category</th>
                  <th className="text-right p-2">Actions</th>
                </tr>
              </thead>
              <tbody>
                {documents.map((doc) => (
                  <tr key={doc._id} className="border-t">
                    <td className="p-2 font-mono text-xs">{doc._id}</td>
                    <td className="p-2">{doc.description}</td>
                    <td className="p-2">{doc.category ?? '—'}</td>
                    <td className="p-2 text-right">
                      <button
                        onClick={() => handleDelete(doc._id)}
                        className="text-red-600 hover:underline"
                      >
                        Delete
                      </button>
                    </td>
                  </tr>
                ))}
                {documents.length === 0 && (
                  <tr>
                    <td colSpan={4} className="p-4 text-center text-gray-500">
                      No documents yet.
                    </td>
                  </tr>
                )}
              </tbody>
            </table>
          </section>
        </div>
      )
    }
    ```

    The example uses plain `<form>`, `<input>`, `<textarea>`, `<table>` styled with Tailwind. Radix UI primitives are available in the template if you want richer controls (dialogs, dropdowns, toasts), but they aren't required for this tutorial.

    Dependencies are minimal: `react` (provided by the template) and the injected `sdk` prop. No extra packages are needed.
  </Step>

  <Step title="Preview the app">
    Switch to **Preview**. Builder compiles the source and renders your SPA inside the device viewport (Desktop / Tablet / Mobile). You should see the upload form and an empty documents table.
  </Step>

  <Step title="Save the page">
    Click **Save**. Builder synchronizes the source files to the workspace.
  </Step>
</Steps>

<img src="https://mintcdn.com/prismeai/K_v8yhAp7bkcmKW-/images/tutorials/builder-workspace-pages.png?fit=max&auto=format&n=K_v8yhAp7bkcmKW-&q=85&s=a4f4c20dda2654463fd8af35b2b26f2e" alt="Builder Pages editor for the index page with Preview / Code toggle, device viewport selectors, and Save button" width="1440" height="900" data-path="images/tutorials/builder-workspace-pages.png" />

*Switch to **Code** to see the file tree (`src/App.tsx`, `src/main.tsx`, `src/styles/globals.css`, `index.html`, `package.json`, `vite.config.ts`) and edit the React source.*

## Step 3: Creating the Document Classification Automation

Next, let's set up the automation that will classify documents when they're uploaded:

<Tip>
  Store your agent ID under workspace **Configuration** as `classifierAgentId` so it's easy to update without editing the automation.
</Tip>

<Steps>
  <Step title="Navigate to Automations">
    In your workspace, go to the Automations section.
  </Step>

  <Step title="Create the Submit Document webhook">
    Create a new automation named "Submit Document" with the following configuration. It exposes an HTTP endpoint, classifies the document, stores it, and emits a `documents.updated` event so any open SPA refreshes.

    ```yaml theme={null}
    slug: submit-document
    name: Submit Document
    when:
      endpoint: true
    do:
      - Agents.sendMessage:
          agent_id: '{{config.classifierAgentId}}'
          message:
            parts:
              - type: text
                text: '{{body.description}}'
          output: agentResponse
      - set:
          name: category
          value: '{{agentResponse.task.output.messages[0].parts[0].text}}'
      - Collection.insert:
          data:
            description: '{{body.description}}'
            attachment: '{{body.attachment}}'
            category: '{{category}}'
          output: inserted
      - emit:
          event: documents.updated
          payload:
            id: '{{inserted._id}}'
    output:
      success: true
      category: '{{category}}'
      id: '{{inserted._id}}'
    ```

    <Tip>
      `Agents.sendMessage` returns an A2A task object. The agent's text reply is at `agentResponse.task.output.messages[0].parts[0].text` — the `set` step above pulls just that string into `category` so the rest of the automation can use it directly. Open the activity log after a first run to see the full task object (status, usage, context ID).

      Make sure the agent uses a model your organization has enabled in **Governe** (e.g. `gpt-4o`). A disallowed model returns an error inside the same `parts[0]` shape with `metadata.error: true`.
    </Tip>

    The classification logic itself (the categories, the tone of the answer) lives on the agent in Agent Creator, not in this automation. To tune the categorization, edit the agent's system instructions there.
  </Step>

  <Step title="Create the List Documents webhook">
    Create another automation named "List Documents". The SPA calls it on mount and after each `documents.updated` event to refresh the table.

    ```yaml theme={null}
    slug: list-documents
    name: List Documents
    when:
      endpoint: true
    do:
      - Collection.find:
          query: {}
          sort:
            createdAt: -1
          output: data
    output:
      documents: '{{data}}'
    ```
  </Step>

  <Step title="Create the Delete Document webhook">
    Create a final automation named "Delete Document". It accepts `{ id }` in the request body, removes the row, and emits `documents.updated` so connected SPAs reload.

    ```yaml theme={null}
    slug: delete-document
    name: Delete Document
    when:
      endpoint: true
    do:
      - conditions:
          '!{{body.id}}':
            - break: {}
          default: []
      - Collection.deleteOne:
          query:
            _id: '{{body.id}}'
      - emit:
          event: documents.updated
          payload:
            id: '{{body.id}}'
            deleted: true
    output:
      success: true
    ```
  </Step>

  <Step title="Deploy Your Automations">
    Save all automations and make sure they're properly configured to work together.
  </Step>
</Steps>

## Step 4: Testing and Using Your Document Classification System

Now it's time to test your document management system:

<Steps>
  <Step title="Access your document upload page">
    Either preview the page directly inside Builder (**Pages** > `index` > **Preview**), or open the deployed URL at `<workspace-slug>.pages.prisme.ai`. The SPA owns its own routing, so there is no `/lang/upload-docs` path anymore.
  </Step>

  <Step title="Upload a test document">
    * Pick a sample document (PDF) in the file input
    * Add a description that gives context about the document's content
    * Submit the form
  </Step>

  <Step title="Verify classification">
    The SPA POSTs to `submit-document`, which returns the inferred category. The `documents.updated` event then triggers a reload of `list-documents`, and the new row appears in the table with its AI-determined classification.
  </Step>

  <Step title="Test document management">
    Click **Delete** on a row to call `delete-document`. The same `documents.updated` event removes it from every open SPA.
  </Step>
</Steps>

<img src="https://mintcdn.com/prismeai/K_v8yhAp7bkcmKW-/images/tutorials/builder-workspace-pages.png?fit=max&auto=format&n=K_v8yhAp7bkcmKW-&q=85&s=a4f4c20dda2654463fd8af35b2b26f2e" alt="Builder Pages editor in Preview mode rendering the workspace's React app" width="1440" height="900" data-path="images/tutorials/builder-workspace-pages.png" />

*The Preview pane on the right renders the compiled SPA. Once `src/App.tsx` is in place, you'll see the upload form and documents table here.*

## Step 5: Version Control and Deployment

To finalize your document classification system:

<Steps>
  <Step title="Push Your Workspace">
    Use **Push** in Builder to save the current state of your workspace to its repository. See [Versioning](/products/ai-builder/versioning) for details on how pushes and versions work.
  </Step>

  <Step title="Review Versions">
    Open the **Versions** tab to browse the workspace history and roll back if needed.
  </Step>

  <Step title="Configure Access Controls">
    Set up appropriate Role-Based Access Control to determine who can use your document management system. See [RBAC](/products/ai-builder/rbac).
  </Step>
</Steps>

## Monitoring and Improving Your System

After deployment, regularly check the system's performance:

<Steps>
  <Step title="Monitor Activity Logs">
    Review the activity logs to track document uploads and classifications.
  </Step>

  <Step title="Review Classification Accuracy">
    Periodically check if documents are being classified correctly and refine your system as needed.
  </Step>

  <Step title="Optimize for Performance">
    Make adjustments to improve speed and accuracy as your document volume grows.
  </Step>
</Steps>

## Extending Your Document Classification System

Your base system is powerful, but consider these enhancements:

* **Advanced Classification**: Refine your agent's system instructions in Agent Creator, or build a more specialized agent per document family
* **Content Extraction**: Extract key information from documents based on their category
* **Automated Workflows**: Trigger specific actions based on document type (e.g., route invoices to accounting)
* **Search Functionality**: Add search capabilities to find documents by content or metadata
* **Integration**: Connect with other systems like CRM or ERP platforms

## Next Steps

<CardGroup cols={2}>
  <Card title="Build a Webhook Handler" icon="webhook" href="/resources/tutorials/webhook-builder">
    Learn how to create webhook integrations for your document management system
  </Card>

  <Card title="Explore AI Contact Routing" icon="phone" href="/resources/tutorials/ai-contact-routing">
    Discover how to route documents to the right department automatically
  </Card>

  <Card title="Create a RAG Agent" icon="database" href="/resources/tutorials/no-code-rag-agent">
    Build an agent that can answer questions about your document repository
  </Card>

  <Card title="Integrate with Websites" icon="globe" href="/resources/tutorials/website-to-rag-agent">
    Connect your document system to web content for enhanced capabilities
  </Card>
</CardGroup>
