Webhooks

A workflow with a webhook trigger gets a webhook URL once it is synced. Each request to it queues one run on a managed runner.

trigger: webhook

The run's trigger input is the request envelope: method, query, headers, and the body as body_base64.

Enable the webhook

Sync the workflow, then open its Settings in the dashboard. Under Webhooks, choose Enable webhook and copy the URL.

oci workflow sync outcome.yml --workspace-id WORKSPACE_ID --create

Treat the URL as a secret: possession allows someone to submit requests to this workflow. Do not commit it or paste it into agent prompts.

curl --fail-with-body "$WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: izzy-test-1' \
  --data '{"message":"Test this workflow"}'

Test on your machine

Before syncing, run the workflow in the container with a saved request. The payload has the same shape a synced webhook run receives:

{
  "schema_version": "outcomeci.trigger.webhook.received/v1",
  "type": "webhook.received",
  "event_id": "local-test-1",
  "received_at": "2026-09-29T09:00:00Z",
  "method": "POST",
  "query": "",
  "headers": {"content-type": "application/json"},
  "body_base64": "eyJtZXNzYWdlIjoiVGVzdCB0aGlzIHdvcmtmbG93In0="
}
oci workflow run --payload webhook.json --auto-continue

Verify provider events

A provider that signs its requests can verify and translate them before a run is queued. Name the provider with uses, point auth at its signing secret, and list the events that start a run:

trigger:
  webhook: {uses: slack, auth: secrets.slack_signing, events: [mention, dm]}
 
secrets:
  slack_signing: vault:slack/signing-secret

Store the signing secret in the workspace Vault and grant it to the workflow. Then set the workflow's webhook URL as the provider's request URL.

For each request, OutcomeCI checks the signature with the signing secret and records the check in the Vault audit log. Then it:

  • answers the provider's URL verification handshake directly;
  • rejects a request with a missing, stale, or wrong signature with 401;
  • acknowledges events the workflow does not listen for without starting a run; and
  • queues a run whose trigger input is the provider's normalized event.

Redeliveries of the same provider event start at most one run, whatever their headers or signature.

Slack supports these events:

Event Starts a run for
mention A top-level message that @mentions the app, in a channel it is in
dm A top-level direct message to the app

Slack requests older than five minutes are rejected. Replies in a thread do not start a run; they belong to the conversation the thread already carries. The trigger input is the event's channel, user, text, and ts.

See Connectors for the providers with receivers.

Delivery guarantees

The API returns 202 Accepted only after it has durably recorded the event and the run it starts. Acknowledgment confirms receipt, not successful execution.

An Idempotency-Key deduplicates an identical request. Reusing it with changed bytes, query, or retained headers returns 409. Reusing the key of a run that failed, was cancelled, or ended uncertain also returns 409; that run is not replayed. Without a key, each request is new. Provider-verified triggers use the provider's event ID instead, and ignore Idempotency-Key.

The typed payload includes schema_version, type, event_id, received_at, method, query, headers, and body_base64. Only the content-type, user-agent, x-github-event, and x-github-delivery headers are kept. Authorization, cookie, and every other header never reach the agent.

Only asynchronous delivery is supported. There is no synchronous forwarding mode or custom HTTP response. Delivery is at least once: idempotency and execution receipts prevent automatic replay, not a universal exactly-once guarantee.

Limits

Condition Response
Body larger than 1 MiB 413
Idempotency-Key over 128 characters or query over 16 KiB 422
A kept header over 4 KiB 422
1,000 runs already waiting for this webhook 429
Signing secret missing, not a single value, or not granted 409

Recovery boundaries

A run that fails for a retryable reason is queued again, up to five times, before it fails. Unstarted work can be reclaimed after an execution lease expires. Started work with a lost lease becomes uncertain and is not automatically replayed.

Inspect uncertain runs and their effects before any manual retry. Durable run history remains available in the dashboard after the run ends.