Skip to main content

Hooks

A hook calls a configured tool at a fixed point of every request, without the model deciding to call it. A typical use is agent memory: recall memories before the model answers, and save the turn after. This page covers hook events, configuration, argument templates, failures, and tools hidden from the model. It is for app developers who configure Quick Apps. It assumes you know tools.

Events​

EventWhen it runsWhat happens to the result
on_request_startBefore the first LLM callAdded to the conversation as a tool call and its result, as if the model had called the tool
on_completionAfter the answer is streamed, before the response closesDiscarded. The hook runs for its side effect only.
  • on_request_start frequency. With append_if_changed, the default, an identical result replaces the earlier pair, and a different result is added as a new pair. With always, a pair is added every turn.
  • on_completion order. These hooks run one after another, in manifest order. The response stays open until they finish, and MCP sessions of the request are still open for them.
  • When on_completion doesn't run. It doesn't run when the turn ends with tool calls handed back to the client.

Configure a hook​

hooks is a list in the manifest:

{
"hooks": [
{
"kind": "tool_call",
"event": "on_request_start",
"name": "recall-memory",
"toolset_name": "memory_server",
"tool_name": "search_memories",
"arguments": { "query": "${last_user_message.content}" }
},
{
"kind": "tool_call",
"event": "on_completion",
"name": "save-memory",
"toolset_name": "memory_server",
"tool_name": "save_memory",
"arguments": {
"user_message": { "$eval": "last_user_message.content" },
"assistant_message": { "$eval": "last_assistant_message.content" }
},
"timeout_seconds": 20
}
]
}
FieldRequiredDefaultDescription
kindYes—tool_call, the only kind
eventYes—on_request_start or on_completion
tool_nameYes—The tool's name in its Toolset, or the exact function name
toolset_nameNo—The Toolset of a REST API or MCP tool. Leave it out for DIAL deployment and internal tools.
argumentsNo{}The tool's arguments, as a template
nameNo—A label for the logs
timeout_secondsNo15 for on_request_start, 30 for on_completionThe time limit of one run
frequencyNoappend_if_changedon_request_start only: always or append_if_changed
refresh_conditionNo—on_request_start only: {"kind": "ttl", "ttl_minutes": N}, to refresh the result after N minutes. It can't be combined with templated arguments.

Argument templates​

arguments is a JSON-e template, rendered against the conversation when the hook runs.

NameEventsValue
messagesBothThe conversation: each message's role, content, tool_calls, and tool_call_id
last_user_messageBothThe last user message
last_assistant_messageBothThe last assistant message without tool calls, or null on the first turn
iteration_counton_completionThe number of orchestrator iterations in the turn
total_tool_callson_completionThe number of tool calls in the turn
  • Text or a typed value. "Q: ${last_user_message.content}" puts the value into text. {"$eval": "iteration_count"} keeps its JSON type: a number, an object, or a list such as messages[-4:].
  • Escaping. Write a literal ${ as $${, and start an object key with a literal $ as $$.
  • Missing values. If an expression can't be evaluated for the request, such as last_assistant_message.content on the first turn, the hook is skipped for that request.
  • Validation. When the manifest is loaded, the syntax and the names of ${...}, $eval, and $if expressions are checked. Other operators are checked when the hook runs.
  • The message list. messages also holds the system prompt, tool results, and pairs added by other features. Message content is text only; attachments aren't included.

To pass only the dialog, filter messages by role. $map drops an element when its $if has no else:

{
"arguments": {
"dialog": {
"$map": { "$eval": "messages" },
"each(m)": {
"$if": "m.role == 'user' || (m.role == 'assistant' && len(m.tool_calls) == 0)",
"then": { "role": { "$eval": "m.role" }, "content": { "$eval": "m.content" } }
}
}
}
}

Failures and timeouts​

A hook never fails the request.

SituationOutcome
The template can't be rendered for this requestThe hook is skipped
The hook runs past its timeoutThe hook is cancelled and skipped, with a warning in the logs
An unexpected errorThe hook is skipped, and the error is logged
The tool failsThe tool's fallback applies. For on_completion, its result is discarded, so the failure shows only in the logs.

Logs name the hook and the event. Arguments and results are never logged.

Tools hidden from the model​

A tool that only hooks should call, such as save_memory, would still be offered to the model, which may start calling it. hidden_from_model on an MCP-based Toolset fixes that: mcp, dial-mcp, or dial-app over MCP.

{
"tool_sets": [
{
"type": "dial-app",
"name": "AppMemory",
"deployment_id": "memory",
"transport": "mcp",
"allowed_tools": ["prime_memories", "save_memory", "search_memories"],
"hidden_from_model": ["prime_memories", "save_memory"]
}
],
"hooks": [
{ "kind": "tool_call", "event": "on_request_start", "toolset_name": "AppMemory", "tool_name": "prime_memories" },
{ "kind": "tool_call", "event": "on_completion", "toolset_name": "AppMemory", "tool_name": "save_memory",
"arguments": { "user_message": { "$eval": "last_user_message.content" } } }
]
}

The model is offered AppMemory_search_memories only, and both hooks run.

  • A hidden tool isn't in the model's tool list or in tool discovery. Hooks call it as usual, and other tools of the same Toolset stay visible.
  • If the model asks for a hidden tool anyway, nothing runs. It gets a tool response saying the platform calls that tool, and the loop continues.
  • Entries are tool names as the MCP server reports them. When allowed_tools is set, every entry must be in it, or the manifest is rejected.
  • A tool the server adds later is visible unless allowed_tools pins the set. Set both.
  • On the chat completion branch of dial-app, hidden_from_model is ignored with a warning.
  • During a rolling upgrade, a replica older than 0.13.0 ignores the field and shows the tools to the model.
SymptomLikely cause
tool '...' not found in initialized toolsThe tool is missing from the server's tool list, or from allowed_tools
Manifest rejected: hidden_from_model entries not present in allowed_toolsAdd the entry to allowed_tools, or remove it from hidden_from_model
The model still sees the toolThe request ran on an older replica

Further reading​