Writing a Custom Template
What you'll learn: How to customize the way framework events map to Kyvvu atomic behaviors — without writing any Python code.
When do you need a custom template?
The built-in templates (decorator, langchain, langgraph, crewai) work out of the box for most agents. You need a custom template when you want to:
Add metadata to specific tools — e.g., mark
fetch_customer_piiascontains_pii: trueso policies can act on PII-handling stepsChange verb classification — e.g., your
archive_recordtool is a destructive DELETE, not the default GETAdd domain-specific patterns — e.g., all tools starting with
db_should be tagged asresource_type: "database"Add new event types — e.g., map a custom callback event to
step.gate
You do not need a custom template to add policies — policies are separate from templates. Templates control what behaviors are emitted; policies control what happens when those behaviors are evaluated.
How templates work
A template is a YAML file with an ordered list of rules. Each rule has:
match— conditions that match against the event context (event name, tool name, etc.)behavior— the Kyvvu atomic behavior to emit when the rule matches
Rules are first-match-wins — the engine tries each rule in order and uses the first one that matches. Order matters.
A custom template replaces the built-in
An integration uses one template, and it governs both the steps the integration records and the task boundaries it opens (task.start, task.end, task.error). A template you pass replaces the built-in for that integration. It does not extend it, and nothing falls through to the built-in for an event your template does not match.
That has two consequences:
Your template must cover every task boundary the integration opens. It is checked when the handler (or the
Kyvvuclient) is built, and a template that cannot do its job raisesKyvvuConfigErrornaming the missing boundaries, instead of failing on the first task. The boundaries are:IntegrationBoundaries the template must matchKyvvu(...)(decorator,start_task,end_task,error_task,idle_task)all four:
task.start,task.end,task.error,task.idle, each matched bystep_typeKyvvuLangChainHandler,KyvvuLangGraphHandlertask.startonon_chain_start;task.endonon_chain_endand onon_agent_finish;task.erroronon_chain_errorKyvvuCrewAIListenertask.startoncrew_kickoff_started;task.endoncrew_kickoff_completed;task.erroroncrew_kickoff_failedStart from a copy of the built-in, so the boundary rules are already there, and change only the rules you care about (next section).
template=None and the default
Every template argument defaults to a sentinel, kyvvu.default_template, which means "the built-in template for this integration, or the file named by KV_TEMPLATE_LOCATION if that is set".
template=None is different: it means no template at all.
Surface
template=None
Kyvvu(template=None): @kv.step and kv.add_step
Accepted. Steps are built from their own arguments (step_type, verb, step_name, properties); no rule runs.
Kyvvu(template=None): start_task() and the other task methods
Refused with KyvvuConfigError: there is no rule to build a task boundary from. Use a framework handler, which brings its own template, or give the client a template.
KyvvuLangChainHandler, KyvvuLangGraphHandler, KyvvuCrewAIListener
Refused with KyvvuConfigError: a callback integration cannot build behaviors without a template.
An event your template does not match
A callback event that the handler treats as a step, and your template has no rule for, is recorded as a step.unknown named after the event's name (or the event itself), and a warning is logged once per event. It is never dropped, because a dropped step is a step no policy ever saw. This is the same outcome as the match: {} catch-all that every built-in template ends with, so you only need your own catch-all if you want to shape that step. The decorator differs: an unmatched decorated call is built from the decorator's own arguments.
Starting from the built-in template
Print any built-in template with the CLI and save it. This works from any install, including a wheel, because it reads the package data rather than a repository path:
kyvvu template show <name> accepts decorator, langchain, langgraph and crewai. An unknown name exits 1 and lists the available ones.
Loading your custom template
Two ways:
Environment variable (recommended for deployment)
Every integration you build with the default picks this file up, with no code changes. Because it then replaces the built-in of each of them, one file has to cover the boundaries of every integration in the process. A langchain template set this way makes Kyvvu(...) itself refuse to build, because the client checks the four task.* types by step_type. When a process mixes the decorator with a handler, pass the template to the handler instead.
Constructor parameter (explicit)
The template goes to the handler, which is the thing that uses it:
Kyvvu(..., template=...) sets the template for the decorator and start_task(), not for the framework handlers.
Template YAML format
A rule has exactly four keys: id, description, match and behavior. The loader is strict and names the rule and the fix when a file breaks one of these:
idis required and must be unique within the template.matchandbehaviorare both required mappings. A catch-all is written as an explicitmatch: {}; leavingmatchout is an error.conditionsandoutputare not keys. They arematchandbehavior, and using them is rejected with a hint.behaviormay only setstep_type,step_name,verbandproperties. Any other key is an error naming the allowed four.A literal
step_typeorverbmust be in the vocabulary, and aname_patternmust be a valid regex.
Match fields
Each key in match is compared with the same key in the event context, and every key must match (implicit AND). There are two kinds of key:
Equality
any context key, including a dot-path such as serialized.name
The context value must equal the value you wrote.
Regex
name_pattern
Tested against the context's name key. Searched anywhere in the value (Python re.search); anchor with ^ and $. Template name patterns are case-insensitive.
name_pattern is the only regex key, and it is the same key on every surface. function_name_pattern was removed; a template that uses it is rejected with a hint to use name_pattern. step_name is an output field (it goes in behavior), never a match key.
A context with no name never matches a name_pattern, whatever the pattern, .* included. A match key that none of the shipped integrations set is allowed, because custom integrations define their own context, but it is logged as a warning when the template loads, so a typo is visible.
A match of {} matches everything (catch-all).
Behavior fields
step_type
string
Atomic behavior type: step.model, step.resource, step.exec, step.gate, step.message, task.start, task.end, etc.
verb
string
GET, POST, PATCH, DELETE, or omit for types that don't carry a verb (step.exec, step.gate, step.unknown, and the four task.* boundary types)
step_name
string
Name for the step. Supports Jinja2-style expressions: {{ name }}, {{ serialized.name }}
properties
dict
Arbitrary metadata attached to the behavior. Policies can inspect these via rule parameters.
String values accept {{ path }}, {{ path | default('x') }} and {{ path | omit_empty }}, which drops the key from the result when the value is missing or empty (see the Template Authoring Guide).
Available context keys
These are available in match conditions and {{ }} expressions. What is in the context depends on the surface that fires the event:
Decorator step (@kv.step)
step_type; verb (when declared); name (the function's name); kwargs (when the function has arguments and capture_input is on)
Task boundaries from Kyvvu
step_type, plus whatever the caller passed as context=; a decorated task.start, task.end, task.error or task.idle function adds name and kwargs
LangChain / LangGraph, steps
event (on_llm_start, on_chat_model_start, on_retriever_start, on_tool_start, on_custom_event, on_message_in, on_message_out, and for KyvvuLangGraphHandler on_interrupt); name; task_id; serialized, metadata and tags for model, retriever and tool events; data for on_custom_event
LangChain / LangGraph, boundaries
step_type, event (on_chain_start, on_chain_end, on_agent_finish, on_chain_error), name; serialized and inputs on on_chain_start
CrewAI
event (crew_kickoff_started, crew_kickoff_completed, crew_kickoff_failed, llm_call_started, tool_usage_started, on_message_in, on_message_out); name; task_id for steps; event details under crewai_metadata
For the decorator, {{ kwargs.* }} means the wrapped function's arguments by parameter name, for the current call. It is not the keyword arguments of the call site: {{ kwargs.url }} resolves only if the function has a parameter called url, and a keyword swallowed by **kw appears under kw.
Common customizations
Mark specific tools as PII-handling
Add a rule before the default tool_read catch-all:
Now you can write a policy that blocks PII tools without a preceding gate.
Classify a tool as destructive (DELETE)
Tag all database tools
Interpolation supports a lookup, default(...) and omit_empty, and nothing else, so a verb cannot be computed from the name. Write one rule per verb, most specific first:
Rule ordering tips
Specific rules first, catch-all last. The first matching rule wins.
Keep the task boundary rules, and do not reorder them behind a broad rule. Your template replaces the built-in, so a copy made with
kyvvu template showalready has them; deleting one fails when the handler is built.Keep the default
tool_defaultcatch-all as the last tool rule. Custom tool rules go betweentool_writeandtool_default.Test with
KV_LOG_LEVEL=DEBUG. The engine logs which rule matched for each event.
Debugging
Set KV_LOG_LEVEL=DEBUG to see template matching in action:
If you see kyvvu: template 'my-template' has no rule for event 'on_tool_start'; recording it as step.unknown, your template is missing a rule for that event type, and the steps are being recorded as step.unknown.
Next steps
LangChain / LangGraph Integration — the built-in handler these templates work with
Atomic Behaviours — the full vocabulary of behavior types
Template Authoring Guide — match semantics, property extraction and a worked example
Writing a New Integration — for non-LangChain frameworks that need a Python adapter
Last updated
