Webhook Event Push¶
Webhooks let automation receive Obra workflow events without polling or holding long-lived SSE connections.
They are the best fit for autonomous operators managing multiple workflows or integrating Obra into existing orchestration systems.
Why webhooks¶
Use webhooks when:
- one agent needs updates from many runs at once
- network policies make long-lived streams inconvenient
- you want push delivery into an existing automation service
- you need signed event payloads for downstream verification
SSE remains useful for live local operator views. Webhooks are better for durable event push into autonomous systems.
Register a webhook¶
curl -X POST http://127.0.0.1:18790/api/webhooks/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"callback_url": "https://automation.example.com/hooks/obra",
"resource_type": "workflow_run",
"resource_id": "run-123",
"events": [
"workflow_run.completed",
"workflow_run.failed",
"workflow_run.blocked"
],
"secret": "replace-with-a-secret-at-least-16-chars"
}'
Request fields:
| Field | Required | Notes |
|---|---|---|
callback_url |
Yes | Must use https, unless target is localhost or loopback |
resource_type |
Yes | workflow_run, workflow_derivation, or workflow |
resource_id |
Yes | Specific resource id to watch |
events |
Yes | One or more canonical event names for that resource type |
secret |
Yes | HMAC secret, minimum 16 characters |
The gateway rejects:
- private IP callback targets
- embedded callback credentials
- invalid resource types
- event names outside the canonical set for the chosen resource type
Event types¶
Registrable event names are grouped by resource_type.
workflow_run¶
| Event name | Meaning |
|---|---|
workflow_run.blocked |
Run is awaiting operator attention |
workflow_run.cancel_requested |
Cancellation was requested |
workflow_run.cancelled |
Run ended in cancelled state |
workflow_run.completed |
Run completed successfully |
workflow_run.failed |
Run failed |
workflow_run.resumed |
Run resumed after a pause |
workflow_run.progress |
Run emitted progress (catch-all for non-promoted actions) |
workflow_run.stage_started |
A pipeline stage started |
workflow_run.stage_completed |
A pipeline stage completed |
workflow_run.stage_failed |
A pipeline stage failed |
workflow_run.phase_started |
A workflow phase started |
workflow_run.phase_completed |
A workflow phase completed |
workflow_run.item_started |
A work item started |
workflow_run.item_completed |
A work item completed |
workflow_run.session_info |
Session bootstrap context emitted |
workflow_run.planning_stage_started |
A planning substage started |
workflow_run.planning_stage_completed |
A planning substage completed |
workflow_run.story0_completed |
First story milestone completed |
workflow_derivation¶
| Event name | Meaning |
|---|---|
workflow_derivation.awaiting_operator |
Derivation is waiting on operator review |
workflow_derivation.cancelled |
Derivation ended in cancelled state |
workflow_derivation.completed |
Derivation completed successfully |
workflow_derivation.failed |
Derivation failed |
workflow_derivation.started |
Derivation started |
workflow_derivation.waiting |
Derivation entered a wait state |
workflow_derivation.progress |
Derivation emitted progress (catch-all for non-promoted actions) |
workflow_derivation.stage_started |
A pipeline stage started |
workflow_derivation.stage_completed |
A pipeline stage completed |
workflow_derivation.stage_failed |
A pipeline stage failed |
workflow_derivation.phase_started |
A derivation phase started |
workflow_derivation.phase_completed |
A derivation phase completed |
workflow_derivation.item_started |
A work item started |
workflow_derivation.item_completed |
A work item completed |
workflow_derivation.session_info |
Session bootstrap context emitted |
workflow_derivation.planning_stage_started |
A planning substage started |
workflow_derivation.planning_stage_completed |
A planning substage completed |
workflow_derivation.story0_completed |
First story milestone completed |
workflow_derivation.derivation_step_started |
A derivation step started |
workflow_derivation.derivation_step_completed |
A derivation step completed |
workflow_derivation.derivation_step_failed |
A derivation step failed |
workflow¶
| Event name | Meaning |
|---|---|
workflow.archived |
Workflow archived |
workflow.created |
Workflow created |
workflow.deleted |
Workflow deleted or moved to trash |
workflow.restored |
Workflow restored |
workflow.updated |
Workflow updated |
workflow.validated |
Workflow validated |
Important distinctions:
webhook.testis emitted only byPOST /api/webhooks/{webhook_id}/testwebhook.testis not a valid registration event- the promoted
workflow_run.stage_*,workflow_run.phase_*, etc. names are first-class transport names and can be used as webhook filter targets - names such as
escalation.createdare not currently registrable webhook event values
Signature verification¶
Outbound deliveries include X-Obra-Signature with an HMAC-SHA256 digest in
sha256=<hex> format.
Example verification in Python:
import hashlib
import hmac
def verify_obra_signature(payload_bytes: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
payload_bytes,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
Recommended handling:
- verify before parsing or acting on the payload
- log failed signature attempts as security events
- rotate the secret by creating a new webhook registration when needed
Retry behavior¶
The gateway retries failed deliveries three times with the default backoff schedule:
- 1 second
- 5 seconds
- 30 seconds
Failure tracking defaults:
active->failingafter 5 consecutive failuresfailing->disabledafter 20 consecutive failures
These values are config-wired defaults and may be overridden by gateway configuration, but they are the shipped baseline.
Auto-cleanup¶
Webhook registrations expire automatically when the watched resource reaches a terminal state:
workflow_run.completedworkflow_run.failedworkflow_run.cancelledworkflow_derivation.completedworkflow_derivation.failed
Expired registrations are retained temporarily for cleanup and audit, then deleted after the configured retention window.
Management routes¶
curl http://127.0.0.1:18790/api/webhooks/ \
-H "Authorization: Bearer <token>"
curl http://127.0.0.1:18790/api/webhooks/<webhook_id> \
-H "Authorization: Bearer <token>"
curl -X DELETE http://127.0.0.1:18790/api/webhooks/<webhook_id> \
-H "Authorization: Bearer <token>"
curl -X POST http://127.0.0.1:18790/api/webhooks/<webhook_id>/test \
-H "Authorization: Bearer <token>"
The test route sends a synthetic payload and reports delivery status, status code, duration, and any delivery error.
End-to-end example¶
1. Register the webhook¶
curl -X POST http://127.0.0.1:18790/api/webhooks/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"callback_url": "https://automation.example.com/hooks/obra",
"resource_type": "workflow_run",
"resource_id": "run-123",
"events": ["workflow_run.completed"],
"secret": "replace-with-a-secret-at-least-16-chars"
}'
2. Launch the run you want to watch¶
curl -X POST http://127.0.0.1:18790/api/workflow-runs/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"objective": "Run the nightly claims exception review",
"project_id": "claims-ops",
"working_dir": "~/obra-projects/claims-ops",
"domain_id": "business"
}'
3. Receive the completion event¶
Example delivery shape:
{
"resource_type": "workflow_run",
"event_id": "run-123:42",
"event_name": "workflow_run.completed",
"sequence": 42,
"timestamp": "2026-03-24T18:30:12.123456+00:00",
"workflow_run_id": "run-123",
"workflow_derivation_id": null,
"payload": {
"summary": "Run completed"
},
"correlation": {
"workflow_run_id": "run-123",
"workflow_derivation_id": null,
"workflow_id": "WORKFLOW-CLAIMS-001",
"workflow_revision_id": "rev-12",
"domain_id": "business",
"checkpoint_id": null,
"escalation_id": null,
"trace_id": null,
"base_revision_id": null,
"current_revision_id": null,
"resulting_revision_id": null
}
}
4. Verify the signature¶
payload_bytes = request_body_bytes
signature = request.headers["X-Obra-Signature"]
assert verify_obra_signature(payload_bytes, signature, secret)