For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 (langchain, decorator) 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_pii as contains_pii: true so policies can detect PII-handling steps

  • Change verb classification — e.g., your archive_record tool is a destructive DELETE, not the default GET

  • Add domain-specific patterns — e.g., all tools starting with db_ should be tagged as resource_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.

Starting from the built-in template

The easiest way to create a custom template is to copy and modify the built-in one. The LangChain template is at:

Copy it to your project:

Loading your custom template

Two ways:

The handler picks this up automatically — no code changes needed.

Constructor parameter (explicit)

Template YAML format

Match fields

Field
Type
Description

event

string

Exact match on the callback event name (e.g., on_tool_start, on_llm_start)

name_pattern

regex

Regex match on the tool/step name. Tested against the name context key.

Both fields are optional. If both are present, both must match. If neither is present, the rule matches everything (catch-all).

Behavior fields

Field
Type
Description

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, PUT, DELETE, or omit for types that don't use verbs (gates, tasks)

step_name

string

Name for the step. Supports Jinja2 templates: {{ name }}, {{ serialized.name }}

properties

dict

Arbitrary metadata attached to the behavior. Policies can inspect these via rule parameters.

Available context keys (LangChain)

These are available in match conditions and {{ }} template expressions:

Key
Source
Example

event

Callback method name

"on_tool_start"

name

Tool/model/chain name

"fetch_customer"

serialized

LangChain serialized dict

{"name": "ChatOpenAI", ...}

inputs

Chain inputs (on_chain_start only)

{"messages": [...]}

outputs

Chain outputs (on_chain_end only)

{"output": "..."}

task_id

Active task ID

"abc-123"

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

Rule ordering tips

  1. Specific rules first, catch-all last. The first matching rule wins.

  2. Keep task lifecycle rules at the top. Don't accidentally match on_chain_start with a tool rule.

  3. Keep the default tool_default catch-all as the last tool rule. Custom tool rules go between tool_write and tool_default.

  4. 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: unmatched event context, your template is missing a rule for that event type.


Next steps

Last updated