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

# Webhooks and Slack

> Push execution events to your own endpoint or into a Slack channel, with filters, signed payloads, and the full event list.

workflows emit events as they run. An **integration** carries those events to somewhere you watch — an
HTTP endpoint you own, or a Slack channel.

Use integrations instead of polling when executions are long or volume is high. Use them when a person
must know that a unattended execution failed.

***

## How events reach you

Integrations work in two layers.

1. **The integration.** You create it once for the organisation. It holds the connection — the
   webhook URL, or the authorised Slack workspace.
2. **The notification.** You attach the integration to a workflow, and choose which events fire it.

One integration can serve many workflows. One workflow can carry several notifications with different
rules.

```mermaid theme={null}
flowchart TD
    Service["Outside service (Slack, your API)"]
    Integration["Integration"]
    NotifA["Notification A"]
    NotifB["Notification B"]
    workflowA["workflow A"]
    workflowB["workflow B"]

    Service <--> Integration
    Integration --> NotifA
    Integration --> NotifB
    NotifA --> workflowA
    NotifB --> workflowB
```

***

## Set up a webhook

<Steps>
  <Step title="Create the integration">
    Go to **Integrations** → **Add Integration** → **Webhook**, then set:

    * **URL** — the endpoint that receives the events.
    * **Headers** — optional headers Asteroid adds to every request.

    Asteroid always sends `POST` with a JSON body.
  </Step>

  <Step title="Attach it to a workflow">
    Open the workflow, go to the **Notifications** tab, and select **Add Notification**. Then configure:

    * **Event rules** — which event types fire the notification.
    * **Field filters** — match on fields inside the payload.
    * **Metadata filter** — fire only when the execution's metadata matches.
    * **Unwrap result** — for `EXECUTION_COMPLETED`, send `payload.result` alone instead of the full
      envelope.
  </Step>

  <Step title="Verify the signature">
    Check `X-Asteroid-Signature` against the raw request body before you parse or act on anything.
    See [Verify the signature](#verify-the-signature).
  </Step>
</Steps>

***

## Payload structure

Every request body has the same shape, unless `Unwrap result` is set on an `EXECUTION_COMPLETED` rule. That delivery carries the result object on its own.

```json theme={null}
{
  "type": "execution",
  "event_id": "83b7b49f-1f73-4f86-9d89-6fd8dd2557f3",
  "timestamp": "2026-03-27T10:30:00Z",
  "info": {
    "event": "EXECUTION_COMPLETED",
    "execution_id": "a2c7f3b3-cbc8-4b6a-a5f2-c5f6cb548e95",
    "execution_url": "https://platform.asteroid.ai/executions/a2c7f3b3-cbc8-4b6a-a5f2-c5f6cb548e95",
    "agent_id": "6bf63aaf-6e3e-4115-8f13-e707d627582f",
    "agent_name": "Data Extraction workflow",
    "metadata": {
      "environment": "production"
    },
    "payload": {
      "result": { "slots": [], "nextAvailable": "2026-04-08" },
      "reasoning": "The portal returned an empty calendar for the requested week.",
      "outcome": "no_slots_available"
    }
  }
}
```

<ParamField body="type" type="string" required>
  The category of the notification. `execution` is the only value.
</ParamField>

<ParamField body="event_id" type="string" required>
  Identifier for this delivery.
</ParamField>

<ParamField body="timestamp" type="string" required>
  ISO 8601 timestamp of the event.
</ParamField>

<ParamField body="info" type="object" required>
  The execution context and the event-specific payload.
</ParamField>

`info` holds:

<ParamField body="info.event" type="string" required>
  The event name. See [Event types](#event-types).
</ParamField>

<ParamField body="info.execution_id" type="string" required>
  Execution UUID.
</ParamField>

<ParamField body="info.execution_url" type="string" required>
  Link to the execution in the platform. For batch events it points at the workflow's batch page.
</ParamField>

<ParamField body="info.agent_id" type="string" required>
  workflow UUID.
</ParamField>

<ParamField body="info.agent_name" type="string" required>
  workflow display name.
</ParamField>

<ParamField body="info.metadata" type="object">
  The metadata you attached when you started the execution.
</ParamField>

<ParamField body="info.payload" type="object" required>
  Event-specific data. Its shape depends on `info.event`.
</ParamField>

***

## Event types

Match on `info.event`. The rule picker in the platform lists the same events by name.

### Execution lifecycle

| `info.event`                      | Payload                          |
| --------------------------------- | -------------------------------- |
| `EXECUTION_STARTED`               | `{}`                             |
| `EXECUTION_COMPLETED`             | `{ result, reasoning, outcome }` |
| `EXECUTION_FAILED`                | `{ reason }`                     |
| `EXECUTION_CANCELLED`             | `{ reason, cancelled_by }`       |
| `EXECUTION_PAUSED`                | `{ reason, paused_by }`          |
| `EXECUTION_RESUMED`               | `{ reason }`                     |
| `EXECUTION_AWAITING_CONFIRMATION` | `{ reason }`                     |

`EXECUTION_CANCELLED` carries the cancel reason. See [Debug your workflows](/operate/debug) for what
each reason means.

### Actions and steps

| `info.event`                 | Payload                                                                  |
| ---------------------------- | ------------------------------------------------------------------------ |
| `EXECUTION_ACTION_STARTED`   | `{ action_id, action_name, arguments, step_number }`                     |
| `EXECUTION_ACTION_COMPLETED` | `{ action_id, action_name, output, step_number, duration? }`             |
| `EXECUTION_ACTION_FAILED`    | `{ action_id, action_name, failure, step_number, duration?, os_error? }` |
| `EXECUTION_STEP_STARTED`     | `{ step }`                                                               |
| `EXECUTION_STEP_PROCESSED`   | `{ step }`                                                               |

### Execution detail

| `info.event`                            | Payload                                                               |
| --------------------------------------- | --------------------------------------------------------------------- |
| `EXECUTION_MESSAGE_ADDED`               | `{ message }`                                                         |
| `EXECUTION_REASONING_ADDED`             | `{ reasoning }`                                                       |
| `EXECUTION_FILE_ADDED`                  | `{ file_id, file_name, mime_type, file_size, source, presigned_url }` |
| `EXECUTION_PLAYWRIGHT_SCRIPT_GENERATED` | `{ node_id, node_name, script, context, generated_at }`               |
| `EXECUTION_TRANSITIONED`                | `{ to_node, from_node_duration?, transition_type? }`                  |
| `USER_MESSAGE_RECEIVED`                 | `{ user_id, message, execution_was, injected_into }`                  |

### Batches and tests

| `info.event`      | Payload                                                      |
| ----------------- | ------------------------------------------------------------ |
| `BATCH_STARTED`   | `{ batch_id, batch_name, item_count }`                       |
| `BATCH_COMPLETED` | `{ batch_id, batch_name, triggered_count, cancelled_count }` |
| `EXECUTION_TEST`  | `{ message }`                                                |

See [Batch executions](/operate/batches).

### Example: a failed action

```json theme={null}
{
  "type": "execution",
  "event_id": "f0df4bd4-fdf8-445b-b870-1cd6ed329f39",
  "timestamp": "2026-03-27T16:10:05Z",
  "info": {
    "event": "EXECUTION_ACTION_FAILED",
    "execution_id": "a2c7f3b3-cbc8-4b6a-a5f2-c5f6cb548e95",
    "execution_url": "https://platform.asteroid.ai/executions/a2c7f3b3-cbc8-4b6a-a5f2-c5f6cb548e95",
    "agent_id": "6bf63aaf-6e3e-4115-8f13-e707d627582f",
    "agent_name": "Data Extraction workflow",
    "payload": {
      "action_id": "node-123",
      "action_name": "click",
      "failure": "Timeout waiting for selector",
      "step_number": 7,
      "duration": 1432,
      "os_error": {
        "message": "Target page, context or browser has been closed"
      }
    }
  }
}
```

***

## Verify the signature

Asteroid signs every body:

* SHA-256 digest of the raw body bytes.
* RSA PKCS#1 v1.5 signature.
* Base64, in the `X-Asteroid-Signature` header.

```
X-Asteroid-Signature: <base64-signature>
```

Use the webhook verification public key for your workspace. Fetch it from
`GET /integrations/webhook/public-key`, and cache it against the `keyId` that call returns.
Refetch when the `keyId` changes, so a rotated key does not start rejecting valid deliveries.

<Warning>
  Verify against the **raw** request bytes, before you parse the JSON. Any reserialisation changes the
  bytes and breaks the check.
</Warning>

```javascript theme={null}
import crypto from "node:crypto";
import express from "express";

const app = express();

function verifyWebhookSignature(rawBodyBuffer, signatureBase64, publicKeyPem) {
  const verifier = crypto.createVerify("sha256");
  verifier.update(rawBodyBuffer);
  verifier.end();
  return verifier.verify(publicKeyPem, signatureBase64, "base64");
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.header("X-Asteroid-Signature");
  if (!signature) {
    return res.status(401).json({ error: "Missing signature" });
  }

  const isValid = verifyWebhookSignature(
    req.body,
    signature,
    process.env.ASTEROID_WEBHOOK_PUBLIC_KEY_PEM,
  );
  if (!isValid) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(req.body.toString("utf8"));
  console.log(event.info.event, event.info.execution_id);

  res.status(200).json({ received: true });
});

app.listen(3000);
```

***

## Filter what you receive

Most teams want the failures, not the running commentary.

### Subscription modes

| Mode                 | Behaviour                                                |
| -------------------- | -------------------------------------------------------- |
| **Subscribe to all** | Every event type, including any type added later.        |
| **Custom rules**     | The event types you pick, with optional filters on each. |

### Field filters

Add one or more field filters to an event type. Filters on one event type combine with **OR**: the
notification fires when any of them match.

Two filters — `outcome equals failure` and `outcome equals cancelled` — mean "tell me when the
outcome is either one".

### Metadata filters

A metadata filter matches the key-value pairs you attached when you started the execution. All pairs must
match, so the filter combines with **AND**.

| Run metadata                               | Filter                                    | Result                        |
| ------------------------------------------ | ----------------------------------------- | ----------------------------- |
| `environment: production`                  | `environment: production`                 | Sent                          |
| `environment: staging`                     | `environment: production`                 | Skipped                       |
| `environment: production, region: us-east` | `environment: production`                 | Sent. Extra keys are ignored. |
| `environment: production`                  | `environment: production, team: payments` | Skipped. `team` is missing.   |

Leave the metadata filter empty and the notification fires for every execution.

Metadata filtering runs after event-type filtering. An execution must match the event rules first, then the
metadata filter.

<Tip>
  Attach metadata on every execute call and one workflow can feed several channels. Send production
  failures to your on-call channel, and staging failures nowhere.
  See [Call a workflow from your code](/integrate/call-an-agent).
</Tip>

### A worked example

One workflow, two environments, two webhooks.

| Integration       | Events                                    | Metadata filter            |
| ----------------- | ----------------------------------------- | -------------------------- |
| Production alerts | `EXECUTION_FAILED`, `EXECUTION_COMPLETED` | `environment = production` |
| Staging alerts    | `EXECUTION_FAILED`                        | `environment = staging`    |

A production failure fires the first. A staging failure fires the second. A staging run that
completes fires neither.

***

## Slack

The Slack integration posts the same events into a channel, formatted for people to read.

### Install it

You need a Slack workspace where you can install apps.

<Steps>
  <Step title="Add the integration">
    In the platform, go to **Integrations** → **Add Integration** → **Slack**. Slack asks you to
    authorise the app. Review the permissions, pick the workspace, and select **Allow**.
  </Step>

  <Step title="Set the default channel">
    Find the new Slack integration in the list and open its **Edit** dialog. Pick a **Default
    Channel** and save. The Asteroid bot joins the channel you pick.

    <Note>
      The dropdown lists public channels. To use a private channel, invite the Asteroid bot to it in
      Slack first. The channel then appears in the dropdown, after you refresh the page.
    </Note>
  </Step>

  <Step title="Attach it to a workflow">
    Open the workflow, go to the **Notifications** tab, and select **Add Notification**. Pick the Slack
    integration, set the event rules, and save.
  </Step>
</Steps>

### What a Slack message contains

* **Status** — a colour and an icon for the event type.
* **Context** — the workflow name, and a link to the execution in the platform.
* **Detail** — depends on the event:
  * `EXECUTION_COMPLETED` — a snippet of the result and the workflow's reasoning.
  * `EXECUTION_FAILED` — the error and the reason.
  * `EXECUTION_PAUSED` — the reason, such as the question the workflow asked.

Some messages carry a button. A human-in-the-loop request lets you open the execution straight from
Slack.

***

## Test the integration

Select **Test integration** in the platform. Asteroid sends an `EXECUTION_TEST` payload.

```json theme={null}
{
  "type": "execution",
  "event_id": "2ec89fb9-c2b5-4f6a-9fe9-c8f4f9dd6f66",
  "timestamp": "2026-03-27T10:30:00Z",
  "info": {
    "event": "EXECUTION_TEST",
    "execution_id": "00000000-0000-0000-0000-000000000003",
    "execution_url": "",
    "agent_id": "00000000-0000-0000-0000-000000000001",
    "agent_name": "Test workflow",
    "payload": {
      "message": "This is a test notification from Asteroid. If you're seeing this, your integration is working correctly! 🎉"
    }
  }
}
```

<Tip>
  Point a new webhook at [webhook.site](https://webhook.site) for the first smoke test. Then move to
  your real endpoint with signature verification switched on.
</Tip>

***

## Build a reliable endpoint

<AccordionGroup>
  <Accordion title="Answer with 2xx quickly">
    Return `2xx` as soon as you have the body. Do the work afterwards, in your own queue. A slow
    endpoint turns into a failed delivery.
  </Accordion>

  <Accordion title="Expect the same event twice">
    Delivery is at-least-once. Build your handler so a repeat is harmless.

    Dedupe on your own business identifiers. `event_id` is generated per delivery and changes across
    retries, so it cannot carry the whole job.
  </Accordion>

  <Accordion title="Know the retry budget">
    Asteroid makes up to three send attempts, with a short backoff, before it marks a delivery
    failed.
  </Accordion>

  <Accordion title="Sweep for what you missed">
    Run a periodic sweep with `GET /executions` alongside your webhook handler. It catches anything
    that arrived while your endpoint was down. See [Executions and statuses](/concepts/executions).
  </Accordion>

  <Accordion title="Log the context">
    Log `info.event`, `info.execution_id`, `info.agent_id`, and your own outcome. That is enough to
    reconstruct any delivery later.
  </Accordion>
</AccordionGroup>

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="No events arrive">
    * Check the endpoint is reachable from the internet and accepts `POST`.
    * Check the notification's event rules cover the events you expect.
    * Check the metadata filter values match the execution's metadata exactly.
  </Accordion>

  <Accordion title="Signature verification fails">
    * Verify against the raw request bytes, not parsed JSON.
    * Use RSA PKCS#1 v1.5 with SHA-256.
    * Check you have the public key for the right workspace.
  </Accordion>

  <Accordion title="The same event arrives twice">
    * Handle at-least-once delivery in your own logic.
    * Dedupe on stable execution fields, not on `event_id` alone.
    * Return `2xx` once you recognise a duplicate.
  </Accordion>

  <Accordion title="Slack messages go nowhere">
    * Check the integration has a default channel.
    * For a private channel, invite the Asteroid bot in Slack first.
    * Check the integration is attached to the workflow on its Notifications tab.
  </Accordion>
</AccordionGroup>

***

## Next

<CardGroup cols={2}>
  <Card title="Call a workflow from your code" icon="code" href="/integrate/call-an-agent" horizontal>Attach the metadata your filters match on</Card>
  <Card title="Batch executions" icon="layers" href="/operate/batches" horizontal>Batch events and why webhooks beat polling</Card>
  <Card title="Executions and statuses" icon="activity" href="/concepts/executions" horizontal>The statuses behind each event</Card>
  <Card title="Debug your workflows" icon="bug" href="/operate/debug" horizontal>What to do when an event says the execution failed</Card>
</CardGroup>
