> ## 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.

# AI Contact Routing

> Build an intelligent contact form that automatically routes inquiries to the right department using Gen.AI

This tutorial guides you through creating an intelligent contact form powered by generative AI. The system automatically analyzes the content of submitted inquiries and routes them to the appropriate department within your organization—be it sales, support, or careers—enhancing operational efficiency and customer response times.

## What You'll Build

A complete AI-powered contact routing system with:

* A customizable contact form for your website or application
* Intelligent content analysis using a governed agent
* Automated routing to department-specific email addresses
* Attachment handling capabilities
* Easy maintenance and monitoring

<Note>
  This solution demonstrates how Prisme.ai can combine AI models with automation workflows to create intelligent business processes that scale with your organization's needs.
</Note>

## Prerequisites

Before starting this tutorial, make sure you have:

* An active Prisme.ai account
* The SendMail app installed in your workspace
* 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 contact form is public — without it, `Agents.sendMessage` returns `401 UNAUTHORIZED` for unauthenticated submissions.
* A routing agent created in Agent Creator with system instructions like: *Given the following customer inquiry, categorize it as either sales, support, or careers. Provide your categorization based on the content. Answer with the category only.*

## Step 1: Creating Your Workspace

First, let's set up a dedicated workspace for our contact routing system:

<Steps>
  <Step title="Access Builder">
    Log in to AI Studio and open **Builder**. Your workspaces list lives at `/builder`.
  </Step>

  <Step title="Create a New Workspace">
    Click **+ Create new workspace** to start the creation wizard.
  </Step>

  <Step title="Choose a Starting Point">
    In step 1 of the wizard, choose **From Scratch**.
  </Step>

  <Step title="Configure Workspace Settings">
    In step 2:

    * Name your workspace **AI Contact Routing**
    * Add a description like "Intelligent contact form with AI-based routing"
    * 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 wizard layout above is the same regardless of name — for this tutorial, fill in "AI Contact Routing".*

## Step 2: Building the Contact Form

Now, let's create a user-friendly contact form that will collect information from your visitors. In v27, each Builder workspace has 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`, `onSubmit: formSubmit`, etc.) is gone. Pages are now React SPA source trees; the platform compiles and previews them inside Builder. Form submission is done with `fetch` against a webhook automation, not by emitting a Prisme.ai event.
</Note>

<Steps>
  <Step title="Open Pages">
    In your workspace, select **Pages** in the Builder sidebar. Open the editable `index` page — that's the workspace's React app.
  </Step>

  <Step title="Switch to Code mode">
    Use the page toolbar to switch to **Code**. You should see the source tree with files such as:

    * `src/App.tsx`
    * `src/main.tsx`
    * `src/styles/globals.css`
    * `index.html`
    * `package.json`
    * `vite.config.ts`

    If the workspace has no source yet, initialize the React/Vite/Tailwind/Radix starter from Code mode.
  </Step>

  <Step title="Replace src/App.tsx with the contact form">
    Open `src/App.tsx` in the editor and replace its content with the SPA below. It renders a Tailwind-styled form with `name`, `email`, `message`, and an optional `attachment` file input. On submit, it reads the file as a base64 data URL (simple option for small attachments) and POSTs everything as JSON to a webhook automation called `submit-contact`.

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

    interface SDK {
      host: string
      token?: string
      _csrfToken?: string
    }

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

    type Status =
      | { kind: 'idle' }
      | { kind: 'sending' }
      | { kind: 'success'; routedTo: string; category: string }
      | { kind: 'error'; message: string }

    function readFileAsDataUrl(file: File): Promise<string> {
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
        reader.onload = () => resolve(String(reader.result))
        reader.onerror = () => reject(reader.error)
        reader.readAsDataURL(file)
      })
    }

    export default function App({ sdk, workspace }: AppProps) {
      const [name, setName] = useState('')
      const [email, setEmail] = useState('')
      const [message, setMessage] = useState('')
      const [file, setFile] = useState<File | null>(null)
      const [status, setStatus] = useState<Status>({ kind: 'idle' })

      async function handleSubmit(e: React.FormEvent) {
        e.preventDefault()
        setStatus({ kind: 'sending' })

        try {
          let attachment: { name: string; type: string; dataUrl: string } | null = null
          if (file) {
            const dataUrl = await readFileAsDataUrl(file)
            attachment = { name: file.name, type: file.type, dataUrl }
          }

          const response = await fetch(
            `${sdk.host}/workspaces/slug:${workspace.slug}/webhooks/submit-contact`,
            {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                ...(sdk.token ? { Authorization: `Bearer ${sdk.token}` } : {}),
                ...(sdk._csrfToken
                  ? { 'x-prismeai-csrf-token': sdk._csrfToken }
                  : {}),
              },
              body: JSON.stringify({ name, email, message, attachment }),
            },
          )

          if (!response.ok) {
            throw new Error(`Request failed with status ${response.status}`)
          }

          const data = (await response.json()) as {
            routed_to?: string
            category?: string
          }

          setStatus({
            kind: 'success',
            routedTo: data.routed_to ?? 'unknown',
            category: data.category ?? 'unknown',
          })
          setName('')
          setEmail('')
          setMessage('')
          setFile(null)
        } catch (err) {
          setStatus({
            kind: 'error',
            message: err instanceof Error ? err.message : 'Unknown error',
          })
        }
      }

      const isSubmitting = status.kind === 'sending'

      return (
        <main className="min-h-screen bg-slate-50 px-4 py-12">
          <div className="mx-auto max-w-xl rounded-2xl bg-white p-8 shadow-sm ring-1 ring-slate-200">
            <h1 className="text-2xl font-semibold text-slate-900">Contact us</h1>
            <p className="mt-1 text-sm text-slate-600">
              Tell us how we can help. Your message will be routed to the right team.
            </p>

            <form onSubmit={handleSubmit} className="mt-6 space-y-4">
              <div>
                <label className="block text-sm font-medium text-slate-700">Name</label>
                <input
                  type="text"
                  required
                  value={name}
                  onChange={(e) => setName(e.target.value)}
                  className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none"
                  placeholder="Jane Doe"
                />
              </div>

              <div>
                <label className="block text-sm font-medium text-slate-700">Email</label>
                <input
                  type="email"
                  required
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none"
                  placeholder="jane.doe@example.com"
                />
              </div>

              <div>
                <label className="block text-sm font-medium text-slate-700">Message</label>
                <textarea
                  required
                  value={message}
                  onChange={(e) => setMessage(e.target.value)}
                  rows={5}
                  className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-slate-500 focus:outline-none"
                  placeholder="How can we help?"
                />
              </div>

              <div>
                <label className="block text-sm font-medium text-slate-700">
                  Attachment (optional)
                </label>
                <input
                  type="file"
                  onChange={(e) => setFile(e.target.files?.[0] ?? null)}
                  className="mt-1 block w-full text-sm text-slate-700 file:mr-3 file:rounded-md file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:text-white hover:file:bg-slate-800"
                />
              </div>

              <button
                type="submit"
                disabled={isSubmitting}
                className="w-full rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-60"
              >
                {isSubmitting ? 'Sending…' : 'Send message'}
              </button>

              {status.kind === 'success' && (
                <p className="rounded-md bg-emerald-50 px-3 py-2 text-sm text-emerald-800 ring-1 ring-emerald-200">
                  Thanks — your message was routed to <strong>{status.routedTo}</strong>{' '}
                  (category: {status.category}).
                </p>
              )}

              {status.kind === 'error' && (
                <p className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-800 ring-1 ring-red-200">
                  Something went wrong: {status.message}
                </p>
              )}
            </form>
          </div>
        </main>
      )
    }
    ```

    <Tip>
      Reading the file as a base64 data URL is the simplest path and works fine for small attachments. For anything larger (or for binary attachments you want to keep separate from the routing payload), upload it via the platform `/files` upload endpoints first and pass only the resulting file URL or metadata in the JSON body — then reference that URL from the email step.
    </Tip>
  </Step>

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

  <Step title="Preview the form">
    Switch back to **Preview** and try the form in the **Desktop**, **Tablet**, and **Mobile** viewports. The compiled SPA renders inside Builder — you don't need to deploy to test the layout.
  </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" />

*The Preview pane renders the compiled SPA in Builder. Once `src/App.tsx` is in place, the contact form appears in the right pane.*

## Step 3: Installing Required Apps

Before creating our automation, we need to install the necessary apps from the Prisme.ai App Store:

<Steps>
  <Step title="Open the Imports panel">
    In your workspace sidebar, open **Imports** and click the **+** button to browse the marketplace.
  </Step>

  <Step title="Install the SendMail App">
    Search for "SendMail" and click to install it. Follow the on-screen instructions to complete the installation.
  </Step>

  <Step title="Install the Agent Factory App">
    Search for "Agent Factory App" in the marketplace; once installed it exposes the `Agents` automation namespace.
  </Step>
</Steps>

## Step 4: Creating the Form Submission Handler

Now, let's build the webhook automation that processes form submissions and routes them based on AI analysis. The page calls it via `fetch`, so we expose it as an HTTP endpoint and read the form fields from `body.*`.

<Steps>
  <Step title="Create a New Automation">
    * Navigate to the "Automations" section of your workspace
    * Click "Create Automation"
    * Name it "Submit Contact"
    * Set the slug to **`submit-contact`** — it must match the URL the SPA POSTs to
  </Step>

  <Step title="Configure the Webhook Trigger">
    Set the trigger to a webhook (HTTP endpoint) instead of an event. In YAML this is `when: { endpoint: true }`. The page reaches it at `${sdk.host}/workspaces/slug:${workspace.slug}/webhooks/submit-contact`.
  </Step>

  <Step title="Set Default Recipient">
    Add a "Set var" instruction:

    * Variable name: "recipient"
    * Value: "[hello@example.com](mailto:hello@example.com)" (a default email in case routing fails)
  </Step>

  <Step title="Call the Routing Agent">
    Add an `Agents.sendMessage` instruction:

    * `agent_id`: `{{config.routingAgentId}}` (set the agent ID under workspace **Configuration**)
    * `message`: `{ parts: [{ type: text, text: '{{body.message}}' }] }`
    * Output: `result`
  </Step>

  <Step title="Extract Agent Response">
    Add another "Set var" instruction:

    * Variable name: "routingDecision"
    * Value: `'{{result.task.output.messages[0].parts[0].text}}'`

    <Tip>
      `Agents.sendMessage` returns an A2A task object. The agent's text reply is at `task.output.messages[0].parts[0].text`. Inspect the activity log after a first run to see the full shape (it also includes `task.id`, `task.contextId`, and `usage`).

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

  <Step title="Create Conditional Routing">
    Add a "conditions" instruction with the following branches:

    * Condition: `{{routingDecision}} = "sales"`
      * Action: Set recipient to "[sales@example.com](mailto:sales@example.com)"
    * Condition: `{{routingDecision}} = "support"`
      * Action: Set recipient to "[support@example.com](mailto:support@example.com)"
    * Condition: `{{routingDecision}} = "careers"`
      * Action: Set recipient to "[careers@example.com](mailto:careers@example.com)"
  </Step>

  <Step title="Configure Email Notification">
    Add a "SendMail.sendMail" instruction:

    * To: `"{{recipient}}"` (the dynamically set department email)
    * ReplyTo: `"{{body.email}}"` (the submitter's email)
    * Subject: "New Contact Form Submission"
    * Body: "Message: `{{body.message}}`, Name: `{{body.name}}`, Attachment: `{{body.attachment.name}}`"
  </Step>

  <Step title="Return a Confirmation to the SPA">
    Set the automation output to an object the page can read:

    ```yaml theme={null}
    output:
      routed_to: '{{recipient}}'
      category: '{{routingDecision}}'
    ```

    The SPA's `handleSubmit` reads `routed_to` and `category` from the response and shows them in the success banner. Save the automation.
  </Step>
</Steps>

## Step 5: Testing Your Contact Routing System

Now it's time to test your AI contact routing system. You have two ways to do it:

* **In Builder Preview** — open the `index` page in **Preview** mode. The compiled SPA runs inside Builder with the workspace context already injected, so the `fetch` call to `submit-contact` works without deploying. This is the fastest first-run check.
* **On the deployed page** — once the workspace is deployed, the public URL is `<workspace-slug>.pages.prisme.ai`.

<Steps>
  <Step title="Access Your Contact Form">
    Open the form in Builder's Preview, or navigate to `<workspace-slug>.pages.prisme.ai` if the workspace is deployed.
  </Step>

  <Step title="Submit Test Inquiries">
    Fill out the form with different types of inquiries to test the routing:

    * A sales-related inquiry (e.g., "I'm interested in pricing for your enterprise plan")
    * A support-related inquiry (e.g., "I'm having trouble logging into my account")
    * A careers-related inquiry (e.g., "I'd like to apply for the marketing position")
  </Step>

  <Step title="Verify Email Routing">
    Check that each test inquiry is routed to the correct department email. The success banner in the SPA also shows the resolved `routed_to` and `category` returned by the automation.
  </Step>

  <Step title="Test Attachment Handling">
    Submit a form with a small attachment so the base64 data URL is included in the JSON payload, then verify the file metadata appears in the email.
  </Step>
</Steps>

<img src="https://mintcdn.com/prismeai/K_v8yhAp7bkcmKW-/images/tutorials/builder-workspace-activity.png?fit=max&auto=format&n=K_v8yhAp7bkcmKW-&q=85&s=c6620f5436f85dcdd8639dde6ec75c62" alt="Builder workspace Activity log listing runtime.automations.executed events for the webhook and downstream calls" width="1440" height="900" data-path="images/tutorials/builder-workspace-activity.png" />

*Each request lands as a `runtime.automations.executed` event with a correlation ID — drill into one to see the `Agents.sendMessage` and `SendMail.sendMail` sub-calls.*

## Step 6: Version Control and Deployment

To finalize your contact routing system:

<Steps>
  <Step title="Push Your Changes">
    Use the **Push** button in your workspace to create a new version. See [Versioning](/products/ai-builder/versioning) for details.
  </Step>

  <Step title="Set Up Access Controls">
    Configure Role-Based Access Control to determine who can access and modify your contact routing system. See [RBAC](/products/ai-builder/rbac) for details.
  </Step>
</Steps>

## Monitoring and Optimization

After deployment, regularly check the system's performance:

<Steps>
  <Step title="Monitor Activity Logs">
    Review the activity logs to track form submissions and routing decisions
  </Step>

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

  <Step title="Optimize the Agent Instructions">
    If necessary, adjust the routing agent's system instructions in Agent Creator to improve classification accuracy for your specific use case
  </Step>
</Steps>

## Extending Your Contact Routing System

Consider these enhancements to make your system even more powerful:

* **Multi-level Classification**: Add subcategories to route inquiries to specific teams within departments
* **Priority Detection**: Use AI to identify urgent inquiries and flag them for immediate attention
* **Sentiment Analysis**: Detect the emotional tone of inquiries to handle frustrated customers appropriately
* **Automated Responses**: Send automated acknowledgment emails based on the inquiry type
* **CRM Integration**: Connect with your CRM system to log inquiries as leads or support tickets
* **Slack Integration**: Send notifications to Slack channels for real-time team collaboration

## Complete YAML Configuration

Here's the complete YAML for the `submit-contact` webhook automation:

```yaml theme={null}
slug: submit-contact
name: Submit Contact
do:
  - set:
      name: recipient
      value: hello@example.com
  - Agents.sendMessage:
      agent_id: '{{config.routingAgentId}}'
      message:
        parts:
          - type: text
            text: '{{body.message}}'
      output: result
  - set:
      name: routingDecision
      value: '{{result.task.output.messages[0].parts[0].text}}'
  - conditions:
      '{{routingDecision}} = "sales"':
        - set:
            name: recipient
            value: sales@example.com
      '{{routingDecision}} = "support"':
        - set:
            name: recipient
            value: support@example.com
      '{{routingDecision}} = "careers"':
        - set:
            name: recipient
            value: careers@example.com
      default: []
  - SendMail.sendMail:
      to: '{{recipient}}'
      replyTo: '{{body.email}}'
      subject: New Contact Form Submission
      body: 'Message: {{body.message}}, Name: {{body.name}}, Attachment: {{body.attachment.name}}'
output:
  routed_to: '{{recipient}}'
  category: '{{routingDecision}}'
when:
  endpoint: true
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Build a Document Classification System" icon="file" href="/resources/tutorials/data-classification-agent">
    Learn how to classify and organize documents with AI
  </Card>

  <Card title="Create Webhook Integrations" icon="webhook" href="/resources/tutorials/webhook-builder">
    Connect your contact system to external services using webhooks
  </Card>

  <Card title="Implement RAG Agents" icon="database" href="/resources/tutorials/no-code-rag-agent">
    Build a knowledge base to provide automated responses to common inquiries
  </Card>

  <Card title="Explore Custom Applications" icon="puzzle-piece" href="/resources/tutorials/ai-builder-custom-apps">
    Develop more sophisticated applications using Builder
  </Card>
</CardGroup>
