Skip to content

React to session end

Watch for sessions that stop with an error and have an agent summarize each one — an event-driven trigger with no clock and no inbound request.

ShippedFor people running agent work6 pages in this section

What you build

A standing watcher over your own runtime. When a session stops with an error, a summarizing agent picks it up, reads what happened, and writes it down — so the failure is explained by the time anyone looks.

Nothing external is involved. The session lifecycle observer publishes session.stopped, the trigger matches it, and a run starts. This is the third way work begins in Compozy, alongside a clock (jobs) and an inbound request (webhooks).

Use it when you want a reaction attached to something the runtime already knows, rather than a poll.

The artifact

config.toml
[[automation.triggers]]
scope = "workspace"
workspace = "/Users/you/src/checkout-api"
name = "summarize-failed-sessions"
event = "session.stopped"
agent = "summarizer"
prompt = "Summarize session {{ index .Data \"session_id\" }}. Stop reason: {{ index .Data \"stop_reason\" }}."
filter = { "data.stop_reason" = "error" }
retry = { strategy = "backoff", max_retries = 2, base_delay = "5s" }
fire_limit = { max = 4, window = "1h" }

The filter is what keeps this quiet. Without it every session that stops — including every clean one — would start a summarizing run.

Run it

compozy automation triggers create \
  --name summarize-failed-sessions \
  --scope workspace \
  --workspace /Users/you/src/checkout-api \
  --event session.stopped \
  --agent summarizer \
  --prompt 'Summarize session {{ index .Data "session_id" }}. Stop reason: {{ index .Data "stop_reason" }}.' \
  --filter data.stop_reason=error \
  --retry backoff:2:5s

Then prove it end to end by making a session fail, and read what the trigger produced:

compozy automation triggers -o json
compozy automation runs -o json

How it works

The observer publishes the activation

The session lifecycle observer emits session.stopped with the session's own data. Alongside it the runtime publishes session.created and memory.consolidated; hook completions arrive as hook.<hook_name>.completed, and extensions publish their own ext.* families.

Five things must line up before it fires

The trigger is enabled, its event equals the activation event, its scope matches, a workspace trigger matches the same workspace id, and every filter entry matches exactly. Any mismatch and nothing happens — no partial run, no log noise.

Filters read the envelope, not the transcript

kind, scope, workspace_id, source, and data.<path> are the available filter paths, and values are compared as exact strings. data.stop_reason = "error" is what separates a failure from an ordinary stop.

The prompt is a Go template over that envelope

.Kind, .Scope, .WorkspaceID, .Source, and .Data are in scope. Use index for dynamic keys, as the prompt above does for session_id and stop_reason. An invalid template fails validation when the trigger is created or the config is loaded — never silently at fire time.

Fire limits stop a feedback loop

A summarizing run is itself a session, and it will stop too. fire_limit caps this trigger at 4 runs per hour, and the data.stop_reason = "error" filter means a clean summarizing session never re-activates the trigger that started it.

Next steps

On this page