Skip to main content

Inbound Triggers (Webhook → Task)

An Inbound Trigger is a standing rule that turns something happening outside the platform into work inside it. Every trigger owns an HTTPS endpoint and a signing secret. Each verified call to that endpoint creates a Task from the trigger's own template, assigns it to the Agent you nominated, and — unless you asked it to wait — starts the agent run immediately.

That is the whole feature. A deploy finishes → a Task. A form is submitted → a Task. A monitor trips at 3 a.m. → a Task, already assigned, already running.

Triggers live under Tasks → Triggers at /tasks/triggers, with a detail page per trigger at /tasks/triggers/:id.

Where to find them

The Tasks surface carries a tab strip — Tasks | Triggers. The Triggers tab lists every trigger you own in the current Organization scope:

ColumnWhat it shows
NameLinks to the trigger detail page; a webhook icon or a lightning icon marks the source, description below
ModeTask or Template — what a fire produces, locked when the trigger was created
TargetThe task-template slug, the title template, or Default task title
EnabledA switch that pauses or resumes the trigger in place
Last FiredRelative timestamp, or Never
FiresLifetime count of real fires (test fires are not counted)

The row menu () holds Fire now, Test fire, Edit, Rotate secret (webhook triggers only) and Delete.

A second, smaller surface exists: the Inbound triggers panel under Activity → Schedules, which can create, pause, rotate and delete triggers but has no detail page or fire log. See Activity.

Two sources: a signed URL, or a platform event

The Source is chosen at create time and cannot be changed afterwards.

SourceFired byEndpoint + secretBest for
Webhook (signed URL)Anyone who can sign a request with the trigger's secretYesCI, monitoring, form back ends, your own scripts, any third-party webhook sender
Platform eventAn ingested platform event whose source / kind / workId match your ruleNoReacting to events that already flow through the platform's ingest spine

An event-sourced trigger needs an Event matcher with at least one field: a producing plugin id (slack-connector), a source-namespaced kind (github.push), or an exact Work id. source and kind accept a trailing * wildcard (github.*), and every field you fill must match. Each event fires a given trigger at most once: the claim on (trigger, event) turns permanent as soon as a fire produces a Task, so a retried ingest drain never double-fires. An attempt that ended Refused or Failed created no Task at all, so it stays re-claimable and a later drain is free to try it again.

Both sources funnel into exactly the same Task-spawning path, so everything below about modes, templates, variables and the fire log applies to either one. Only the delivery half differs.

How to create a trigger

  1. Go to Tasks → Triggers (/tasks/triggers) and click New Trigger.
  2. Give it a Name (up to 120 characters) and, optionally, a Description of what fires it.
  3. Pick the SourceWebhook (signed URL) or Platform event. For an event trigger, fill at least one field of the Event matcher.
  4. Pick the Mode:
    • Task (single-task) — you write Agent instructions, and each delivery's payload is appended to them for the agent to work from.
    • Template — the Task is built from a Task template slug you supply. The slug is required up front; the Create button stays disabled without it.
  5. Optionally set a Task title template and a Task description template (see Titles and descriptions).
  6. Choose an agent under Assign agent. Without one, fires still create Tasks — they just sit unassigned.
  7. Declare a payload contract under Expected variables if you want deliveries validated (see Declaring a payload contract).
  8. Set the operational switches: First task (Start automatically or Leave in backlog), Replay window (seconds) (default 300, range 1086400), Show on task board, and Enabled.
  9. Click Create. For a webhook trigger the Trigger secret dialog opens with the Webhook URL and the Signing secret, each with a copy button.
The signing secret is shown exactly once

Copy it before you close the dialog. Neither the list, the detail page, nor the API ever returns secret material again — the only way to get a working secret back is Rotate secret, which mints a new one.

Mode and Source are locked after creation. Editing a trigger shows both as read-only text; the API rejects a mode field on an update outright. Everything else — name, description, agent, templates, variables, auto-start, replay window, board visibility — stays editable.

Signing a delivery

The fire endpoint is deliberately public: external systems authenticate with the trigger's HMAC secret, never with a session. Send the JSON payload to the webhook URL with these headers:

HeaderRequiredValue
x-everworks-timestampYesUnix epoch seconds (millisecond stamps are also accepted). This exact string is part of what you sign.
x-everworks-signatureYesHex HMAC-SHA256 over `${timestamp}.${rawBody}`, keyed with the secret. A sha256= prefix is allowed; hex case is normalized.
content-typeIn practiceapplication/json (with or without a charset) or application/x-www-form-urlencoded — see the caution below.
x-everworks-deliveryNoYour own id for this delivery. When present it is the deduplication identity; without one the signature stands in.

The signed string is timestamp, a literal dot, then the raw request bytes — not a re-serialized body. This is the sample the trigger detail page generates for you under Signed curl example, with your trigger's URL already filled in:

TS=$(date +%s)
BODY='{"example":"payload"}'
SIG=$(printf "%s.%s" "$TS" "$BODY" | openssl dgst -sha256 -hmac "$TRIGGER_SECRET" -r | cut -d" " -f1)
curl -X POST "$TRIGGER_URL" \
-H 'content-type: application/json' \
-H "x-everworks-timestamp: $TS" \
-H "x-everworks-signature: $SIG" \
-d "$BODY"
Only JSON and form-urlencoded bodies can be verified

The raw bytes are captured by the JSON and urlencoded body parsers. A body sent as text/plain or application/…+json arrives with nothing captured to compare against, so the signature never matches and the call is rejected with 401 — even when your signature is perfectly computed. Send application/json.

The replay window

Replay window (seconds) — 300 by default, adjustable between 10 seconds and 24 hours — does two jobs at once:

  • Freshness. A signed timestamp more than that far from now, in either direction, is rejected.
  • Duplicate suppression. A repeat of the same delivery inside the window answers 200 with duplicate: true and the original Task's id, instead of creating a second Task. Retrying with the same x-everworks-delivery id is the reliable way to get this; a byte-identical retry with the same timestamp signs identically and is also caught, but a retry with a fresh timestamp is a new delivery by definition.

What the endpoint answers

StatusMeaning
200Verified. Body is { ok, taskId, taskSlug }, or { ok, taskId, taskSlug: null, duplicate: true } on a repeat
400Payload over 64 KB, malformed JSON under a JSON content type, or a payload missing a required variable
401Bad or missing signature, or a timestamp outside the replay window — one constant shape for every failure
404No such trigger id
409The trigger is paused

The order is deliberate: 404 is decided before the signature check, and 409 and 400 are only ever shown to correctly-signed callers. A prober holding no secret learns nothing but 401. The fire endpoint is rate-limited to 120 requests per minute; the management endpoints to 30.

What happens on a fire

Every delivery path — signed webhook, matched platform event, Fire now, Test fire — runs this same sequence, so the four cannot drift apart. The full chain, from a signed call through the spawned Task into a real agent run linked back to that Task, is pinned end-to-end by apps/web/e2e/flow-inbound-trigger-task-agent-chain.spec.ts.

Two behaviours worth knowing:

  • Dispatch is best-effort. If a credits gate or an in-flight limit refuses the agent run, the Task that was legitimately created is not undone; the fire is recorded as Done rather than Running, and you can start the Task by hand.
  • A stale agent does not break a fire. If the target agent was archived or deleted, the Task is still created and the call still answers 200.

Where the spawned Task shows up

Trigger-spawned Tasks are hidden from the task board and the default task list unless the trigger has Show on task board switched on. That is deliberate: a webhook firing a hundred times a day should not bury your human backlog.

To reach a hidden Task, use View task on the matching row of the trigger's recent-fires log, or ask the API for it explicitly with GET /api/tasks?includeHidden=true. Once open it is an ordinary Task in every respect — transition it, chat on it, re-run it with an agent.

Declaring a payload contract

Expected variables is the contract a delivery must satisfy. Write one entry per line in the trigger form:

repo*
branch | Branch name
run_id
  • A bare key is informational.
  • A trailing * marks the key required.
  • Anything after | is a display label.

Keys are top-level payload keys matching [A-Za-z0-9_-] (1–64 characters); labels are capped at 80 characters and a trigger may declare at most 20 entries.

A delivery whose payload omits a required key — or carries it as null, undefined or blank — is refused before any Task exists: the caller gets 400, and the fire log records the row as Refused with the missing key named. The agent is never handed half a payload to improvise around.

Titles and descriptions

The default Task title is Trigger: {name}, where {name} expands to the trigger's name. Your own Task title template and Task description template support the same placeholders:

PlaceholderResolves to
{name}The trigger name (the original webhook-era shorthand, still supported)
{{trigger.name}}The trigger name
{{event.kind}}For webhook fires, webhook.fire; for event fires, the ingested event kind
{{event.payload.<key>}}One top-level payload key
{{event.<field>}}id, source, title, actorName, sourceUrl, subjectType, subjectExternalId, occurredAt, workId

Substitution is a single pass of plain string work — no evaluation, and a substituted value that happens to look like a placeholder is inserted verbatim rather than expanded again. An unknown-but-well-formed path renders as empty; a malformed one is rejected when you save the template. Individual values are capped at 500 characters and titles at 200.

The Task body is chosen in this order:

  1. Task mode with instructions — your instructions, then the delivery payload as JSON inside a <webhook_body> block, introduced by an explicit line telling the agent to treat that block as data and not as instructions. Every < inside the payload is emitted as its unicode escape, so a payload containing a literal </webhook_body> cannot close the block early and get itself read as an instruction. The embedded payload is capped at 16,000 characters.
  2. A description template — yours, or the one a resolved task template supplies, rendered with the placeholders above.
  3. Otherwise a provenance dump: which trigger fired, its id, the UTC timestamp, and the payload in a fenced JSON block.
Template mode runs ahead of the task-template catalog

Template mode stores a task-template slug and resolves it at fire time through an optional lookup. While no task-template catalog is bound, a template-mode trigger degrades gracefully — it renders its own title and description templates instead of failing every fire. Slug-linked templates light up with no change to your trigger once the catalog is connected.

Operating a trigger

The detail page (/tasks/triggers/:id) is where day-to-day operation happens: Fire now and Pause/Resume at the top, then the Webhook panel (URL, copy button, the collapsible signed curl example, Rotate secret), then Recent fires.

ActionCreates a TaskCounts as a fireDispatches the agentNeeds a signatureLogged as
Signed webhook callYesYesPer First taskYesWebhook
Matched platform eventYesYesPer First taskNoPlatform event
Fire nowYesYesPer First taskNo — you are signed inManual
Test fireYes, labelled trigger-testNoNeverNo — you are signed inTest

Test fire is the rehearsal: it renders your templates against a sample payload built from the trigger's own declared variables (so a contract-carrying trigger cannot trip its own gate), creates a real, clearly labelled Task, and stops there — no agent run, no counter movement. Fire now is the real thing: same sample payload, but the full production path, counters and all.

How to rotate a signing secret

  1. Open the trigger at /tasks/triggers/:id.
  2. Click Rotate secret in the Webhook panel and confirm the prompt.
  3. Copy the new secret from the reveal panel — it is shown once.
  4. Roll your callers over to it within 24 hours.

Rotation is not a hard cutover: the previous secret keeps verifying for a 24-hour grace window, so senders can migrate on their own schedule. After that only the new secret is accepted. Rotating twice inside the same window does not leave two old secrets alive — each rotation overwrites the stored previous one, and the older secret stops working immediately.

Pause, resume, delete

Pause — the Enabled switch on the Triggers tab, or the button on the detail page — leaves everything intact but makes fires answer 409 and matched events do nothing. Resume puts it back. Delete, in the row menu on the Triggers tab, asks for confirmation and is irreversible: the webhook URL stops working the moment it completes, and later fires on that id answer 404. Tasks already spawned are untouched.

The recent-fires log

The detail page lists the 50 most recent fires, newest first, each with a status chip, the origin, the timestamp, a View task link when a Task exists, and the reason when one does not.

StatusMeaning
RunningTask created and the target agent was dispatched
DoneTask created; no dispatch (no agent, Leave in backlog, or a refused agent run)
FailedTask creation itself failed; the reason is recorded
RefusedThe delivery did not satisfy the trigger's required variables; nothing was created
OriginWhere the fire came from
WebhookA signed call to the public fire endpoint
Platform eventAn ingested event that matched the event rule
ManualFire now
TestTest fire

A Refused reason names only the missing keys, never their values; a Failed reason carries the underlying Task-creation error message. The log has no pagination beyond the 50-row cap and no status filter yet.

Triggers in Activity → Schedules

Every trigger also appears in the unified Schedules view (Activity → Schedules, filter chip Inbound trigger). Because triggers are event-driven rather than clock-driven, the row shows the fixed cadence On event and an empty next run; Last run is the trigger's last fire, and the Owner link points at the target agent when it has one. See Activity for the rest of that view.

Not to be confused with

This pageSomething else
Inbound triggers — traffic coming in, spawning TasksOutbound webhooks — the platform's own events being pushed out to systems you subscribe on your side
The public fire endpoint you own and signThe Trigger.dev receiver at /api/webhooks/trigger/:tenantId, an internal receive-only endpoint for a tenant's background-job project — never a place to point your own webhooks

Scope and ownership

Triggers belong to the user who created them, inside the Organization scope that was active at the time (personal scope lists the triggers with no organization). Every management route on someone else's trigger answers 404 rather than 403, so a stranger cannot even confirm that an id exists. A Task spawned by an anonymous, correctly signed call belongs to the trigger's owner, not to the caller — and so do the agent runs that follow from it.

API reference

Method + pathWhat it does
GET /api/inbound-triggersList your triggers (never any secret material)
POST /api/inbound-triggersCreate one; the response carries the raw secret once
GET /api/inbound-triggers/:idRead one
PATCH /api/inbound-triggers/:idUpdate name, description, agent, templates, variables, auto-start, replay window
POST /api/inbound-triggers/:id/rotate-secretMint a new secret (returned once); the old one verifies for 24 hours
POST /api/inbound-triggers/:id/pausePause — fires then answer 409
POST /api/inbound-triggers/:id/resumeResume
POST /api/inbound-triggers/:id/test-fireRehearsal Task labelled trigger-test; no dispatch, no counters
POST /api/inbound-triggers/:id/fire-nowOwner-initiated real fire
GET /api/inbound-triggers/:id/firesThe 50 most recent fires
DELETE /api/inbound-triggers/:idDelete, irreversibly
POST /api/inbound-triggers/:id/firePublic, HMAC-signed delivery endpoint

Everything except the last row is session-authenticated and scoped to you.

  • Tasks — what every fire produces, and everything you can do with it afterwards.
  • Agents — the worker a trigger hands its Tasks to.
  • Activity — the Schedules view, and the smaller Inbound triggers panel that lives there.
  • Outbound Webhooks — the opposite direction: platform events pushed out to you.
  • Notifications — for when you want to be told something rather than have work created.
  • API reference: Tasks, Agents.