Cadence
Guides

Take work in with an inbound webhook

Register a webhook token, point an inbound rule at the receiver, and turn an outside delivery into a task.

An outside system can push work into Cadence: it posts an event to a webhook address you give it, and a rule you write turns that event into a task. This guide walks the whole path — mint the address, write the rule, and send a delivery to watch a task appear.

It uses the built-in generic source, which accepts any JSON body, so you can follow it with nothing more than curl. For the full set of tools, actions and statuses, see Inbound.

Before you start

Every step that configures the space needs admin. See Authentication. You also need a workflow for the incoming tasks to land on.

One thing to know before you mint anything: the webhook address is unauthenticated. The token in its path is the only thing standing between the open internet and your space, so treat the whole URL as a secret — give it only to the system that will post to it.

Replace https://cadence.example.com with your instance's address.

1. Register a webhook token

Mint the address the outside system will post to with inbound_webhooks_register. Give it a source — a free-form name for the system that will use it; here, generic:

// inbound_webhooks_register
{ "space_key": "OPS", "source": "generic" }

The response carries the full receiver URL, including the plaintext token:

{
  "inbound_webhook": {
    "id": 42,
    "space": "OPS",
    "source": "generic",
    "label": null,
    "revoked": false,
    "revoked_at": null,
    "created_at": "2026-05-12T10:04:00Z"
  },
  "url": "https://cadence.example.com/webhooks/generic/mcwh_3f2b9c7de1a04f68b2c1d5e9"
}

The url is shown once. The token is stored hashed and cannot be recovered, so copy it now; if you lose it, revoke this token and mint a new one. Listing your tokens later returns their metadata but never the token again.

2. Write a rule that turns a delivery into a task

A delivery on its own does nothing — a rule decides what it becomes. A rule matches a topic (the kind of event) and runs an action. Every delivery on the generic source arrives under the topic generic.received, and the action that creates a task is upsert-task.

The easiest place to write this rule is the browser, because the form gives you a picker for the workflow and named boxes for each field. In your space's Integrations settings, open New inbound rule and set:

  • Topic — generic.received.
  • Action — upsert-task.
  • Workflow — pick the workflow the task should land on.
  • External id — an idempotency key for the task, as a template over the delivery's data, for example {{ data.id }}. The first delivery with a given external id creates one task; a later delivery with the same id updates that same task rather than creating another.
  • Title — the task's title, also a template, for example {{ data.title }}.

The External id, Title and optional Body boxes are Liquid templates rendered against the delivery's fields, which you read as {{ data.<field> }}. Leave the filter empty to match every delivery on the topic. Save with Create rule.

A rule is rejected at save time — the form shows the reason and creates nothing — when:

  • the workflow you chose is not in this space: Action params workflow references a workflow that is not in this space;
  • a required field is missing: Action params external_id is required (and the same for title);
  • the topic is not one Cadence knows: Topic is not a registered topic: ….

Over MCP, inbound_rules_create does the same thing — useful for setting a rule up from a program:

// inbound_rules_create
{
  "space_key": "OPS",
  "topic": "generic.received",
  "action_name": "upsert-task",
  "action_params": {
    "workflow": "31",
    "external_id": "{{ data.id }}",
    "title": "{{ data.title }}"
  }
}

Over MCP the workflow is given by its numeric id, not its slug — which is why the browser form, with its picker, is the simpler way in. See Inbound for every field and action.

3. Send a delivery and watch a task appear

Post a JSON body to the URL from step 1. Its top-level fields are what your templates read:

curl -X POST https://cadence.example.com/webhooks/generic/mcwh_3f2b9c7de1a04f68b2c1d5e9 \
  -H "Content-Type: application/json" \
  -d '{ "id": "order-5591", "title": "Refund requested for order 5591" }'

The receiver answers 200 with an empty body and returns straight away — it stores the delivery and processes it afterwards, so the response tells you the delivery was accepted, not that a task was made. It answers 404 instead if the token is unknown, revoked, or does not match the generic in the path, and 413 if the body is larger than 1 MB.

Because processing happens after the response, confirm the outcome with inbound_deliveries_list:

// inbound_deliveries_list
{ "space_key": "OPS", "source": "generic" }

A delivery that matched your rule and created a task reads:

{
  "inbound_deliveries": [
    {
      "id": 5001,
      "space": "OPS",
      "source": "generic",
      "status": "processed",
      "processing_result": "matched inbound rule 17 (upsert-task)",
      "topic": "generic.received",
      "received_at": "2026-05-12T10:07:31Z",
      "created_at": "2026-05-12T10:07:31Z"
    }
  ]
}

The task is now on the workflow you chose, titled from your template. Post the same body again and the external_id keeps it to one task — the second delivery updates that task instead of creating a duplicate.

Once the rule is in place, this runs with no one in the loop: every matching delivery becomes or updates a task on its own. Two things to watch, because they create no task and are visible only in the delivery list:

  • No rule matched. A delivery whose topic or filter no rule matches is recorded with processing_result no rule matched and makes nothing. A 200 from the receiver does not mean a rule ran.
  • A template referred to a field that was not there. Templates fail loudly: if title reads {{ data.title }} and a delivery carries no title, the delivery is marked failed with the render error in processing_result, and no task is created. Template every field against data the deliveries actually carry.

On this page