Skip to content

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.test is emitted only by POST /api/webhooks/{webhook_id}/test
  • webhook.test is 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.created are 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 -> failing after 5 consecutive failures
  • failing -> disabled after 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.completed
  • workflow_run.failed
  • workflow_run.cancelled
  • workflow_derivation.completed
  • workflow_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)