Skip to main content

Hooks

Declarative condition → action pairs that fire on runtime events. Declare them under runtime.hooks[] (whole app) or agents[].hooks[] (one agent).

Always quote YAML "on" — YAML 1.1 reads bare on: as the boolean true.

Mental model​

Every hook is: on an event, if a condition holds, do an action.

yaml
runtime:
hooks:
- id: block-rm-rf
"on": tool_start # the event
condition:
type: content_contains # the test…
keyword: "rm -rf" # …and its params, INLINE (see below)
action:
type: gate # what to do…
allow: false # …and its params, INLINE
reason: "Remove specific paths, not the tree."

Params are inline. A condition's and an action's parameters are siblings of type, NOT nested under a params: key. {type: gate, allow: false, reason: …} is correct; {type: gate, params: {allow: false}} is not. The compiler validates every required param and its type, so a missing one fails the build with a clear message (hook action "inject_message" has no "content") rather than silently misbehaving at runtime.

Hook fields​

FieldTypeNotes
idstringRequired, unique within the app.
"on" (or event)eventThe event to fire on. Both spellings work.
conditionobject{type, …params}. Omit ⇒ always.
actionobject{type, …params}.
cooldownnumberMin seconds between fires.
max_firesintegerCap total fires per session.
priorityintegerHigher runs first when several match.
enabledbooleanfalse keeps the hook in the file but off.
timeoutnumberSeconds budget for a detached (async) action (default 30).
tagsstring[]Free labels.

Events​

"on" accepts either the canonical name or an alias — they compile to the same event.

CanonicalAliasFires when
turn_startuser_promptA user turn begins.
turn_end—A turn finishes (model done for this round).
stoppre_finishThe agent is about to end the turn — the place to gate completion.
tool_startpre_tool_useJust before a tool runs — the place to gate or rewrite a call.
tool_endpost_tool_useJust after a tool returns.
session_start—A session is created.
session_end—A session ends.
pre_compact—Just before context compaction.
error—A runtime error is raised.
approval_request—An approval is being requested — can auto-allow/deny.

Declared but NOT fired in this build — a hook on one of these compiles with a warning (DGT-W0006 … the hook will not fire) and does nothing. Do not build on them:

EventWhy
agent_spawn, agent_completeMulti-agent lifecycle hooks are not implemented in this build.
activationDeclared-only; not routed at the hook layer.

Conditions​

Every condition and its parameters (required ones marked ·):

typeParamsTrue when
always / (omitted)—Always.
never—Never.
context_pressure·threshold (0–1)tokens_used / max_tokens exceeds threshold.
turn_count·threshold (int), every (int)turn == threshold; with every, turn ≥ threshold && turn % every == 0.
tool_calls·threshold (int)Tool calls so far ≥ threshold.
message_count·threshold (int)Messages so far ≥ threshold.
tool_name·match (string or list)The invoked tool matches. Glob: *, ?, and a|b|c alternation.
tool_failed—The tool that just ran errored.
content_contains·keyword (string)keyword is a substring of the model text, the user message, the tool's command arg, or the tool error.
error_type·match (regex)The error type matches the regex.
expression·expr (string)A numeric compare (>, >=, <, <=, ==) on one of: tokens_used, max_tokens, messages, tool_calls, turn_count, open_tasks; or the literals true / false / tool_failed.
all_of·conditions (list)Every nested condition holds (AND).
any_of·conditions (list)Any nested condition holds (OR).
not·condition (object)The nested condition is false.

Actions​

type and its params. The Effect at column is the events where the action's effect is actually applied — an action fired at any other event runs but its turn-changing effect is dropped (e.g. a gate on turn_end never blocks).

typeParamsDoesEffect at
inject_message·content, role(=user), placeholderAdds a message to the conversation.any event
gateallow(=false), reasonBlocks (allow:false) or allows the gated step; reason is shown to the agent.tool_start, stop, approval_request
transform_params·transformation (object)Merges rendered keys into the tool's arguments.tool_start
transform_result·transformation (object)Merges rendered keys into the tool's result.tool_end
lsp_diagnosepath_field, content_fieldRuns the compiler on the edited file and appends its diagnostics to the tool result.tool_end
module_action_injectmodule, ·action, params, role(=user)Calls a tool and injects its output back as a message.any event
compact_contextstrategy (truncate|summarize), keep_last (int)Compacts the session context.any event
module_actionmodule, ·action, paramsCalls a tool; result discarded (side effect only).any (detached)
log·message, levelWrites a server log line.any (detached)
notifytitle, message, level, tagEmits a user-visible notification event.any (detached)
shell·command, cwd, timeout, on_error (log|ignore|raise)Runs bash.run.any (detached)
pipe·to, map (object), extra (object), on_errorCalls the to tool with map+extra as args.any (detached)
chain·actions (list)Runs several actions in order; effects merge.per sub-action
noop—Does nothing.—

compile_yaml and auto_test_deploy are reserved action names: they pass validation but are not implemented yet and do nothing at runtime. Use lsp_diagnose for compile feedback.

Templating​

These action fields are rendered through {{…}} at fire time (everything else is a literal): log.message; notify.title / notify.message; inject_message.content; gate.reason; shell.command / shell.cwd; transform_params.transformation.*; transform_result.transformation.*; pipe.map.*; module_action(.inject).params.*.

Available variables: {{tool.name}}, {{tool.error}}, {{tool.result}} / {{tool.result.KEY}}, {{tool.params}} / {{tool.params.KEY}} (dotted paths and [i] indices walk into the object), {{tasks.summary}}, {{tasks.open}}. A variable that does not resolve renders as empty.

Automatic built-in hooks​

Two hooks are attached by the runtime — you do not declare them:

  • digitorn.builtin.lsp_diagnose — any app that grants the lsp module gets compiler diagnostics appended to the result of every filesystem.write / edit / multi_edit automatically. (This is why the Builder Assistant sees errors after each app.yaml edit without asking.)
  • digitorn.builtin.task_completion_guard — gates stop while the agent has open tasks, nudging it to finish or pause via ask_user.

Verified examples​

Each of these compiles clean (digitorn lint).

yaml
runtime:
hooks:
# Keep a long session alive: summarise old turns past 80% context.
- id: auto-compact-at-80pct
"on": turn_end
condition: { type: context_pressure, threshold: 0.80 }
action: { type: compact_context, strategy: summarize, keep_last: 6 }
cooldown: 60

# Safety gate: refuse a destructive shell command BEFORE it runs.
- id: block-rm-rf
"on": tool_start
condition:
type: all_of
conditions:
- { type: tool_name, match: "bash.run" }
- { type: content_contains, keyword: "rm -rf" }
action:
type: gate
allow: false
reason: "Refusing `rm -rf`. Remove specific paths explicitly instead."

# Push recovery guidance into the turn when any tool errors.
- id: guide-on-tool-error
"on": tool_end
condition: { type: tool_failed }
action:
type: inject_message
content: "Tool `{{tool.name}}` failed: {{tool.error}}. Diagnose before retrying; don't repeat the same call."

# Escalate repeated infra errors to a visible notification.
- id: notify-on-timeout
"on": error
condition: { type: error_type, match: "timeout|rate_limit" }
action:
type: notify
title: "Agent degraded"
message: "Hit a {{tool.error}} — retrying with backoff."
level: warning

# Enrich the turn: after a web.fetch, call another tool with params drawn
# from the call, and inject its output back for the model to use.
- id: enrich-after-fetch
"on": tool_end
condition: { type: tool_name, match: "web.fetch" }
action:
type: module_action_inject
module: web
action: search
params: { query: "background on {{tool.params.url}}" }

# Two effects on one trigger, every 10th turn past 30.
- id: remind-on-long-sessions
"on": turn_end
condition: { type: turn_count, threshold: 30, every: 10 }
action:
type: chain
actions:
- { type: log, message: "Long session: {{tasks.open}} open task(s)." }
- type: inject_message
content: "You're 30+ turns in. Re-read the goal and close open tasks before adding new work."