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: webhookThe 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 --createTreat 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-continueVerify 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-secretStore 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.