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

# n8n

> Wire a bot to an n8n workflow — verification, replies, and the response_url pattern.

n8n is the most capable of the three no-code tools: it can reply in the same request, call the `response_url`, and upload files.

## Set up the trigger

<Steps>
  <Step title="Add a Webhook node">
    Set **HTTP Method** to `POST`. Copy the Production URL into your bot's webhook settings.
  </Step>

  <Step title="Set Response Mode">
    Change **Respond** to **Using 'Respond to Webhook' Node**.

    <Warning>
      Leave this on the default, and n8n instantly replies `Workflow was started` — before your workflow has done anything. We know that exact body and block it. So you get no bot message and no error, just silence. This one setting is the top cause of "my n8n bot says nothing".
    </Warning>
  </Step>

  <Step title="Add a Switch node">
    Route on `{{ $json.body.event }}`. You will want at least `webhook.verify` and `message.created`.
  </Step>
</Steps>

The Webhook node puts the payload under `body`, so the visitor's text is `{{ $json.body.message.text }}`.

## Pass verification

On the `webhook.verify` branch, add a **Respond to Webhook** node:

* **Respond With**: `JSON`
* **Response Body**:

```json theme={null}
{ "challenge": "{{ $json.body.challenge }}" }
```

Press **Verify** in your bot settings while the workflow is **active**. Extra keys in the reply are fine, so n8n's own additions do no harm.

<Note>
  The Test URL only listens while you watch the canvas. Verify against the **Production URL**, with the workflow turned on.
</Note>

## Reply in the same request

For anything under 10 seconds, end with a **Respond to Webhook** node:

* **Respond With**: `JSON`
* **Response Body**:

```json theme={null}
{
  "text": "Your order **#4821** has shipped! 🎉\nExpected delivery: Tuesday.",
  "buttons": [
    { "label": "Track package", "value": "4821" },
    { "label": "Contact support" }
  ]
}
```

Plain string labels are fine — we build `action_id` values from them (`Track package` becomes `track_package`).

## Reply later

An AI Agent or a slow API will blow past the 10-second limit. Confirm first, answer later.

<Steps>
  <Step title="Confirm right away">
    Put a **Respond to Webhook** node straight after the trigger, and set **Respond With** to **No Data**. The typing dots stay on.
  </Step>

  <Step title="Do the slow work">
    Your AI Agent, HTTP calls, database lookups — up to 45 minutes.
  </Step>

  <Step title="POST the answer">
    Add an **HTTP Request** node:

    * **Method**: `POST`
    * **URL**: `{{ $('Webhook').item.json.body.response_url }}`
    * **Body Content Type**: `JSON`
    * **Specify Body**: `Using JSON`
    * **JSON**:

    ```json theme={null}
    {
      "mid": "reply-{{ $('Webhook').item.json.body.event_id }}",
      "blocks": [
        { "type": "text", "text": "{{ $json.output }}" }
      ]
    }
    ```

    No auth header — the key is already in the URL.
  </Step>
</Steps>

<Tip>
  Writing `$('Webhook').item.json.body.response_url` matters: after an AI Agent node, plain `$json` no longer holds the first payload.
</Tip>

### Why the mid

A `mid` turns the POST into an update. If n8n retries the node, or the workflow runs twice, you get one message instead of two. Build it from `event_id`, and it stays the same across retries of one event.

## Show progress

POST to the same `response_url` again and again, with the same `mid`:

<CodeGroup>
  ```json 1. Working theme={null}
  {
    "mid": "job-42",
    "blocks": [
      { "type": "status", "state": "in_progress", "label": "Generating", "progress": 40 }
    ]
  }
  ```

  ```json 2. Done theme={null}
  {
    "mid": "job-42",
    "blocks": [{ "type": "status", "state": "success", "label": "Report ready" }]
  }
  ```
</CodeGroup>

The visitor sees one bubble change, instead of three piling up.

## Send a file

Two HTTP Request nodes.

<Steps>
  <Step title="Upload">
    * **Method**: `POST`
    * **URL**: `{{ $('Webhook').item.json.body.response_url }}/files`
    * **Body Content Type**: `Form-Data (multipart/form-data)`
    * Add a parameter of type **n8n Binary File**, named `file`

    The reply contains `file.file_id`.
  </Step>

  <Step title="Post the message">
    * **URL**: `{{ $('Webhook').item.json.body.response_url }}`
    * **JSON**:

    ```json theme={null}
    {
      "blocks": [
        { "type": "text", "text": "Here's your report 📊" },
        { "type": "file", "file_id": "{{ $json.file.file_id }}", "name": "report.pdf" }
      ]
    }
    ```
  </Step>
</Steps>

## Handle button clicks

Clicks arrive at the same webhook with `event` set to `interaction.button_clicked`. Add a Switch branch and read:

* `{{ $json.body.action_id }}` — which action
* `{{ $json.body.value }}` — your data
* `{{ $json.body.message_metadata }}` — whatever you attached to the message

Reply the same two ways: a **Respond to Webhook** node, or a POST to that event's own `response_url`.

## Fixing problems

<AccordionGroup>
  <Accordion title="The bot says nothing at all">
    The Webhook node's **Respond** setting is still on its default, so n8n replied `Workflow was started` before your nodes ran. Switch it to **Using 'Respond to Webhook' Node**.
  </Accordion>

  <Accordion title="Verification fails with bad_challenge">
    Check the workflow is **active** and you are using the **Production URL**. The wizard shows what we received — usually n8n's default body, which means your Respond node never ran.
  </Accordion>

  <Accordion title="Duplicate messages">
    Retries. Set a `mid` built from `event_id`, and re-posts become updates.
  </Accordion>

  <Accordion title="Everything times out">
    You are doing the work before replying. Confirm with **No Data** first, then POST to the `response_url`.
  </Accordion>

  <Accordion title="The reply is a literal [object Object]">
    You mapped an object into a text field. `{{ $json.output }}` must be text. Use `{{ JSON.stringify($json.output) }}` if you really want the JSON.
  </Accordion>
</AccordionGroup>
