Skip to content

Events & Webhooks

Every notable action inside Soulacy, including messages, tool calls, workboard runs, and reasoning steps, is published as a versioned JSON envelope. Hook your dashboards, alerting, and log shippers into the stream instead of polling the API.

Three ways to consume events:

  1. Signed webhooks (hooks: in config.yaml): easiest, works with the default in-process queue.
  2. WebSocket /ws/events: what the GUI uses; great for live tooling.
  3. NATS subjects: set queue.backend: nats and subscribe from any process or machine.

The envelope (schema v1)

{
  "schema": 1,
  "id": "9b2f6c1e-…",
  "type": "run.failed",
  "agent_id": "support-bot",
  "session_id": "wb-12-1765043210",
  "ts": "2026-06-06T21:04:05Z",
  "data": { "task_id": 12, "run_id": 3, "attempt": 2, "failure_reason": "…" }
}

data carries the type-specific payload verbatim: an object, a string, or null. New event types and fields may be added at any time; consumers must ignore unknown types and fields. Renaming or removing a field bumps schema, with the previous schema dual-published for at least two releases. Full contract: docs/EVENTS.md.

Event types

Type Emitted when data highlights
message.in a run starts (inbound message) full inbound message
message.out the agent replies full reply message
tool.call the LLM requests a tool name, arguments
tool.result a tool returns name, content, is_error
llm.result one LLM turn completes model, input_tokens, output_tokens, duration_ms, tool_calls
llm.route.decision the router evaluates candidates task class, candidates, rejected routes, decision
llm.route.fallback a provider/model fails over source, destination, categorized reason
context.prepared prompt context is ready sources, role counts, model limit, estimated tokens
context.compacted old evidence is compressed or removed to fit same as context.prepared + dropped count
lifecycle.* a compiled-in lifecycle hook runs stage, denied state, reason/error when present
error a run errors (incl. plugin load failures at boot, stage: plugin-load) stage, error
run.started a Workboard run begins task_id, task_title, run_id, attempt
run.finished a Workboard run succeeds same as run.started
run.failed a Workboard run fails + failure_reason
run.artifact a run produced a file + path, size_bytes, tool
reasoning.start a reasoning strategy begins strategy details
reasoning.step one reasoning step completes step details
reasoning.result the strategy concludes final result
rulebook.updated an agent's procedural rulebook gains a new version version info

Queue subjects

Envelopes are published on the configured queue backend (queue: config) under:

soulacy.events.<type>        e.g. soulacy.events.run.failed

Subscribe with NATS wildcards:

  • soulacy.events.>: everything
  • soulacy.events.run.*: workboard run lifecycle only
  • soulacy.events.tool.*: tool activity only

With the default memory backend, subjects exist in-process only (used by the webhook dispatcher). Set queue.backend: nats to consume events externally.

Signed outbound webhooks (hooks:)

Declare endpoints in config.yaml; the dispatcher consumes the queue and POSTs matching envelopes:

hooks:
  - on: [run.failed, "tool.*"]       # exact, "x.*" prefix, or "*"
    agents: [support-bot]             # optional agent filter; empty = all
    url: https://ops.example.com/soulacy
    secret_env: SOULACY_HOOK_SECRET   # env var holding the HMAC secret
Key Description
on Event types to deliver: exact (run.failed), prefix (tool.*), or * for all
agents Only deliver events from these agents (empty = all agents)
url Endpoint that receives POST with the envelope as JSON body
secret_env Name of the env var holding the HMAC signing secret

Each delivery carries these headers:

X-Soulacy-Event:     run.failed         # envelope type
X-Soulacy-Delivery:  <envelope id>      # unique per emission
X-Soulacy-Signature: t=1765043210,v1=<hex hmac-sha256>

The signature is HMAC-SHA256 over "<t>.<raw body>" with your secret. Verify by recomputing and comparing in constant time, and reject timestamps older than 5 minutes (replay guard). Reference implementation: hooks.VerifySignature in internal/hooks.

Retries: failed deliveries (non-2xx or network error) retry up to 5 times with exponential backoff + jitter (1s → 10m cap). After exhaustion the delivery is dropped and a webhook.dead warning is logged.

Best-effort, not at-least-once

Publishing never blocks the engine. If the internal buffer fills (broker down), events are dropped with a warning. Anything that must not be lost should read the durable stores (action log, costs DB) rather than the event stream.

WebSocket stream (/ws/events)

The same events the GUI renders, available to your own tools:

# wscat example: browser WebSockets can't set headers, so the
# credential may be passed as a query parameter:
wscat -c "ws://localhost:18789/ws/events?api_key=sy_..."

Authentication uses the same engine as the REST API. Scoped plugin tokens are accepted only when the plugin's manifest grants the events.subscribe capability; plugin tokens without that grant get 403.

Broadcasting is non-blocking: a slow client's queue fills and events are dropped for that client rather than stalling the agent engine.

See also