# Triggers and webhooks

> What starts a workflow run — webhook, schedule, or Kafka topic — and how to point an external system at your webhook URL.

A trigger is the reason a run exists. Every workflow that is not a subflow needs
at least one before it can be activated, and every run records which trigger
started it.

## Trigger types

Triggers are created through the wizard that opens with a new workflow, never
from the step palette. Its first screen offers four choices, three of which
create a trigger:

| Choice | What it does |
|---|---|
| **Webhook** | Receives HTTP POSTs from external systems. Requires a service account for auth. |
| **Scheduled** | Runs on a recurring interval, cron-like. |
| **Kafka** | Consumes messages from a Kafka topic. Requires a Kafka connector. |
| **No trigger (subflow)** | Creates a reusable subflow invoked by other workflows. |

One more trigger type exists — change data capture, which appears as **CDC** in
the **Type** filter on the workflows list — but it has no card in the wizard
today.

Afterwards, select the trigger node on the canvas to see and change its
configuration in the inspector.

## Webhook triggers

### Set one up

Pick **Webhook** in the wizard. The second screen asks for a **Service
account**, and it is mandatory — Hodoflow does not accept unauthenticated
ingestion. Only active service accounts holding the `webhooks` scope are listed,
and the help text beneath the picker explains why:

> External callers must present a JWT issued from this service account to fire
> the webhook.

Create one first at **Settings → Service Accounts** if you need to. See
[Service accounts](/help/credentials-connections/service-accounts).

Save the workflow. Until you do, the trigger inspector says *Save workflow to
generate webhook token* — the token is minted on save. Afterwards the inspector
shows the URL in a read-only **Webhook URL** field.

### Call it

The canonical form is token-only. The token identifies the trigger, the
organization, and the workspace all by itself, so nothing in the URL depends on
your organization's name.

```bash
curl -X POST https://app.example.com/api/v1/webhooks/YOUR_TRIGGER_TOKEN \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"order_id": "A-123", "total": 250}'
```

`$ACCESS_TOKEN` is a short-lived JWT you exchange your service account's client
ID and secret for, and it must carry the `webhooks` scope.
[Service accounts](/help/credentials-connections/service-accounts) walks through the exchange.

An accepted delivery returns `202 Accepted` with the id of the run it started:

```json
{"status": "accepted", "workflow_run_id": "0198f2c1-..."}
```

The other answers you can get are `401` when the credential is missing or
wrong, `403` when it authenticated but lacks the `webhooks` scope, `404` when
no active trigger matches the token, and `422` when the payload could not be
processed. A `404` is also what an inactive workflow's webhook looks like, since
only active triggers are served.

### Retries and duplicates

Hodoflow recognises a redelivery of something it already accepted and answers
`202` with `{"status": "duplicate"}` instead of starting a second run. That way
a sender retrying an acknowledgement it never saw stops retrying, without
double-processing.

Identity comes from whichever of these headers your sender provides:
`Idempotency-Key`, `X-GitHub-Delivery`, `X-Shopify-Webhook-Id`, or
`X-Twilio-Idempotency-Token`. With none of them, Hodoflow falls back to hashing
the token and the raw body inside a two-hour window, so a byte-identical
payload is treated as a duplicate within that window and processes normally in
a later one.

### Limits

Ingestion is capped at 100 requests per minute per trigger token, and bodies at
1 MB. Both are enforced before any work is done: over the rate you get a `429`
with a `Retry-After` header, and over the size you get a `413`.

### Discovering the payload shape

The steps below a trigger can only offer you fields they know about, so the
inspector has an **Output Schema** section with a **Refresh Schema** button.
Press it and Hodoflow starts listening, showing a **Test URL (no data saved)**
and *Waiting for a test POST request...*. Post a representative payload to that
URL and the field names appear as *N fields found*. Nothing is stored and no run
is created.

### Verifying the sender

A webhook trigger can also point at an inbound connector, chosen under
**Inbound Authentication** in the inspector, to verify signatures or API keys on
top of the service account's token. The picker only appears when your
organization has inbound connectors that carry an authentication method — see
[Credentials and connections](/help/credentials-connections).

## Scheduled triggers

Pick **Scheduled** and give it either an interval in minutes or a cron
expression. The inspector afterwards shows the schedule in plain language with
an **Edit schedule** button to change it.

An interval below one minute is rejected, as is a cron expression that does not
parse. Deactivating the workflow cancels its scheduled jobs; reactivating
recreates them.

## Kafka triggers

Pick **Kafka** and choose a **Kafka connector**. The topic and consumer group
live on the connector, not the trigger, so several workflows can read the same
configured topic without repeating its settings. Set the connector up first at
**Connectors → Inbound** — see [Credentials and connections](/help/credentials-connections).

The Kafka trigger inspector has **Sample schema from topic**, which reads a
message off the topic to populate the fields your downstream steps and mappings
will offer.

Activating a workflow starts its consumers; deactivating stops them.

## Going deeper

The wire-level reference — request and response bodies, every status code,
header names, and the token exchange — is on the developer portal at
[webhook ingestion](/developer/webhook-ingestion).
