Skip to content

Automation V2 Webhooks

Automation V2 workflows can be triggered by external webhooks. For publicly authorable workflows, an accepted webhook starts a new Automation V2 run at the eligible root nodes. A workflow can also declare a typed correlated webhook wait; a matching delivery wakes that wait and resumes the same run. Tandem verifies every delivery, resolves the tenant from the stored trigger (never from the payload), and records the delivery durably. Payloads are treated as untrusted external event data, never as instructions.

For the complete state, routing, approval, inspection, and recovery model, see Building Stateful Workflows in Tandem.

A webhook trigger declares a signature scheme that decides how deliveries are authenticated:

  • hmac_sha256_v1 — Tandem generates the signing secret and reveals it once at creation. Deliveries carry X-Tandem-Webhook-Signature: t=<timestamp_ms>,v1=<hmac_sha256>, an HMAC over the timestamp and raw body. (tandem_hmac_sha256_v1 is the internal verifier/header identifier, not the value you set on the trigger.)
  • github_hmac_sha256 — Tandem generates a secret that you configure on the GitHub webhook. Tandem verifies X-Hub-Signature-256: sha256=<hex>, an HMAC-SHA256 over the exact raw request body.
  • notion_hmac_sha256 — the provider (Notion) owns the signing secret. No secret is revealed at creation; the token arrives out of band and Tandem stores it. Tandem verifies X-Notion-Signature: sha256=<hex> over the exact raw request body. See Notion webhooks below.
  • linear_hmac_sha256 — the provider (Linear) owns the signing secret. Import the Linear-generated secret into the Tandem trigger, then Tandem verifies the bare-hex linear-signature header over the exact raw request body. See Linear issue webhooks below.
  • shared_secret_header_v1 — Tandem compares the value of X-Tandem-Webhook-Secret with the trigger secret. This authenticates the request with a shared token; it does not HMAC the request body.
  • unsigned_dev_mode — accepts a request without a signature only when the server has explicitly enabled unsigned development webhooks. Use this only for local development. Never expose an unsigned trigger on a hosted, production, or otherwise internet-facing callback URL.

For every authenticated scheme, signatures or shared tokens are compared in constant time, and missing, malformed, or mismatched credentials are rejected. Body-signing providers must deliver the exact bytes they signed. The tenant, workspace, deployment, automation, and authority are resolved only from the stored trigger.

Webhook intake is decoupled from workflow execution. An accepted delivery is written to a durable inbox first, then drained to start a new run or wake a matching wait that the runtime has already registered from a public Automation V2 wait node. This keeps intake fast and resilient: a delivery is not lost if the runtime is briefly busy, and duplicate deliveries (same body) do not queue a second run.

Webhook waits do not jump to an arbitrary node. The wait node must already be eligible in the published DAG, and its correlation must match the accepted delivery by provider event ID, idempotency key, or body digest. See Author durable wait nodes for the complete schema and timeout rules.

Callback URLs, identifiers, and browser preflight

Section titled “Callback URLs, identifiers, and browser preflight”

Use the exact callback_url returned by the trigger management API or shown in the Control Panel. The public callback ends with the whpub_... public path token:

  • Direct local Engine: http://127.0.0.1:39731/webhooks/automations/whpub_...
  • Hosted through the Tandem Control Panel: https://test.tandem.ac/api/engine/webhooks/automations/whpub_...

Do not substitute the trigger management ID into the callback path:

  • whpub_... is the public routing token used in the callback URL. It is not a signing credential.
  • whtr_... identifies the trigger in authenticated management APIs.
  • whsec_... is secret signing material. Store it in a secret manager and send it only in the configured signature or shared-secret header.

Webhook delivery is designed primarily for server-to-server calls. curl does not enforce CORS. A browser sends an OPTIONS preflight before a cross-origin POST; the hosted proxy forwards that preflight to the Engine, and the Engine allows only origins configured by TANDEM_CORS_ORIGINS. Do not embed a whsec_... secret in public browser code. If a trusted internal browser client must send webhooks, add its exact origin to TANDEM_CORS_ORIGINS and keep the secret outside user-accessible JavaScript.

This example targets the hosted callback. For a direct local Engine, replace CALLBACK_URL with the local URL above.

Terminal window
read -rsp "Webhook secret (whsec_...): " TANDEM_WEBHOOK_SECRET
printf "\n"
CALLBACK_URL="https://test.tandem.ac/api/engine/webhooks/automations/whpub_your_public_token"
EVENT_ID="manual-$(date +%s)-$RANDOM"
BODY="{\"event\":\"customer.incident_reported\",\"occurrence_id\":\"$EVENT_ID\",\"customer_id\":\"acme-001\",\"summary\":\"Elevated 5xx errors affecting checkout\"}"
curl --include --silent --show-error --fail-with-body \
--request POST \
"$CALLBACK_URL" \
--header "Content-Type: application/json" \
--header "X-Tandem-Webhook-Secret: $TANDEM_WEBHOOK_SECRET" \
--header "X-Tandem-Webhook-Event-ID: $EVENT_ID" \
--data-binary "$BODY"
unset TANDEM_WEBHOOK_SECRET

An HTTP 202 Accepted means the delivery was durably accepted for asynchronous processing. It does not guarantee that a new run was created. Tandem deduplicates replayed payloads by their idempotency claims, including the body digest. The same JSON body can therefore return 202 with delivery status duplicate and no second run. Cancelling or completing the original run does not clear that dedupe record. For a deliberate second test occurrence, change the JSON body (for example, generate a new occurrence_id) and recompute any body-based HMAC signature.

Tandem can receive Notion webhooks directly for Automation V2 workflows — no bridge service required. Notion’s model differs from Tandem’s standard webhook: Notion owns the signing secret. Notion sends a one-time verification_token to your callback URL, you copy that token back into Notion to activate the subscription, and subsequent events are signed with it.

Standard Tandem webhookNotion webhook
Who generates the secretTandem (revealed once at create)Notion (sent to your callback URL)
Signature headerX-Tandem-Webhook-SignatureX-Notion-Signature
Signed contenttimestamp + bodyraw request body
Activationimmediatepaste the verification token back into Notion

Notion event payloads are signals, not full snapshots — use the entity IDs in the event and fetch the latest content through an authorized Notion connector when you need page/database/comment bodies.

  1. Create the workflow. Build (or open) an Automation V2 workflow.
  2. Open Webhooks. In the automation’s webhook manager, create a trigger with provider notion. Tandem forces the notion_hmac_sha256 signature scheme. No secret is revealed at creation — the trigger status shows Waiting for Notion verification token.
  3. Copy the callback URL shown for the trigger.
  4. Paste it into Notion. In your Notion connection’s Webhooks tab, create a subscription pointing at the callback URL.
  5. Wait for the token. Notion POSTs a verification_token to the callback URL. Tandem stores it (as the trigger’s signing secret), records a notion_verification_token_received delivery, and the status advances to Verification token received. This request does not start a workflow run.
  6. Reveal and paste the token back. In Tandem, click Reveal verification token (available exactly once) and paste it into Notion to verify the subscription. Tandem never shows the token again.
  7. Trigger an event. Once Notion sends a signed event, Tandem verifies X-Notion-Signature and records the delivery. It starts a new Automation V2 run when no registered wait matches, or wakes the matching typed webhook wait and resumes that existing run. The status advances to Verified — receiving signed events.
  8. Confirm. The accepted delivery appears in Recent deliveries and links to either the queued run or the woken run and wait.
  • Signatures are HMAC-SHA256 over the exact raw body, keyed by the stored verification token, compared in constant time. Missing, malformed, or mismatched signatures are rejected.
  • The tenant is resolved only from the stored trigger; the Notion payload never selects tenant, workspace, deployment, automation, or authority.
  • The verification token is stored tenant- and trigger-scoped, revealed at most once to an authorized owner/admin, and never returned again or logged.
  • Duplicate events (same body) do not queue a second run.
  • The verification token is only captured while the trigger is awaiting one; an unsigned request cannot overwrite a token that has already been received.

Tandem can receive Linear issue events directly for Automation V2 workflows when the trigger uses provider linear and the native linear_hmac_sha256 signature scheme. Use this for repair-loop automations where a Linear issue should trigger an ACA workflow without a bridge service.

Linear webhooks are team- or workspace-scoped. Treat the signed Linear payload as trusted for origin only. Before ACA receives authority to inspect or modify a repository, use a fixed read-only guard lookup to check the current project, label, and issue state. The webhook event action remains operator-inspectable metadata; it is not currently a node input.

  1. Create or select the workflow. Build an Automation V2 workflow that starts with a Linear guard node before any repo, MCP, or write-capable ACA step.
  2. Create the Tandem trigger. In the automation’s webhook manager, create a trigger with provider linear, event kind such as issues.updated, and signature scheme linear_hmac_sha256.
  3. Copy the callback URL shown for the trigger.
  4. Create the Linear webhook. In Linear, open Settings -> API -> Webhooks, create a webhook, paste the Tandem callback URL, and select Issues data change events. If Linear asks for teams, select the team that owns the repair project.
  5. Import the Linear signing secret. Copy the signing secret generated by Linear and paste/import it into the Tandem trigger. Tandem stores it as tenant- and trigger-scoped secret material; the secret should not be committed to workflow JSON, docs, screenshots, or demo notes.
  6. Verify a test delivery. Create or update a test issue in the intended project. The delivery should show provider linear, scheme linear_hmac_sha256, a Linear delivery/event id when available, body digest, verification reason, delivery outcome, and queued run id when a run was created. Follow that run id to inspect the guard output.
  7. Rotate exposed secrets. If the Linear signing secret was pasted into chat, logs, screenshots, or a public demo environment, rotate or recreate the Linear webhook secret and re-import the new value into Tandem.

Do not use unsigned_dev_mode for any public Linear callback URL. That mode is only for explicitly enabled local/dev servers and should fail closed on hosted or internet-facing deployments.

Use the first workflow node as an authority boundary. The guard should accept only the configured Linear project, an explicit repair-ready label such as tandem:repair-ready, and an eligible current issue state. A valid delivery can start the run, but the guard returns has_work: false when its authoritative lookup finds no eligible issue, suppressing the downstream ACA nodes.

For a reusable template, see Automation Examples for Teams.

SymptomLikely causeFix
missing_signatureLinear did not include linear-signature, or the request hit the wrong URL.Recopy the Tandem callback URL into Linear and verify the webhook uses Linear’s normal JSON delivery path.
malformed_signatureThe signature header is not the expected Linear HMAC value.Recreate the Linear webhook or remove any proxy/header rewrite between Linear and Tandem.
bad_signatureWrong imported secret, mutated body, stale secret after rotation, or a proxy changed bytes before Tandem verified them.Re-import the current Linear signing secret and make sure the raw body reaches Tandem unchanged.
missing_secret_materialThe trigger uses linear_hmac_sha256 but no Linear secret has been imported.Import the Linear signing secret into the Tandem trigger; the trigger should fail closed until this is done.
stale_signature_timestampLinear’s webhookTimestamp is outside the accepted clock-skew window.Check server clock drift and avoid replaying old payloads.
Delivery accepted but no ACA work runsThe webhook run started, but a fixed read-only guard lookup found no eligible issue, or the delivery was deduplicated/suppressed before a new run was created.Inspect delivery/run metadata and the guard output, then update the authoritative Linear project/label state or intentionally rerun.
Public test only works with unsigned_dev_modeThe trigger is bypassing production signature verification.Switch to linear_hmac_sha256, import the Linear secret, and rotate any secret exposed during testing.

Each queued run stores webhook metadata under automation_snapshot.metadata.automation_webhook: provider, event type, entity id, trigger_id, delivery_id, body_digest, and the verification scheme, with trust: "untrusted_external_webhook". This is an operator inspection surface. Current public Automation V2 authoring does not inject that object into a root node prompt or expose it as an input_refs source; use a fixed read-only provider lookup for node-visible guard decisions.

MethodPathPurpose
POST, OPTIONS/webhooks/automations/{public_path_token}Direct Engine public intake and browser preflight.
POST, OPTIONS/api/engine/webhooks/automations/{public_path_token}Hosted Control Panel public intake and browser preflight.
POST/automations/v2/{id}/webhook-triggersCreate a webhook trigger (e.g. a notion trigger).
POST/automations/v2/{id}/webhook-triggers/{trigger_id}/reveal-verification-tokenOne-time reveal of a Notion verification token (admin-scoped).
POST/automations/v2/{id}/webhook-triggers/{trigger_id}/import-secretImport/replace a Linear signing secret (admin-scoped).
GET/automations/v2/{id}/webhook-triggers/{trigger_id}Trigger status incl. verification_status.

SDK:

  • Notion: client.automationsV2.revealWebhookVerificationToken(automationId, triggerId).
  • Linear: client.automationsV2.importWebhookProviderSecret(automationId, triggerId, secret).

For the full Linear dev reference (troubleshooting, run metadata, secret rotation), see Using Linear Webhooks with Tandem.

  • Notion idempotency uses the request body digest (Notion has no stable event-id header); payload-id-based dedup could be added later.
  • The Notion verification token is captured only while the trigger is awaiting one; to re-capture (e.g. after re-subscribing in Notion) recreate the trigger.
  • Linear project and label scoping is intentionally handled by workflow guards, not by trusting provider scope alone.