> For the complete documentation index, see [llms.txt](https://docs.kyvvu.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kyvvu.com/integrations/custom-template.md).

# 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 act on 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:

```
kyvvu-sdk/kyvvu/templates/langchain.template.yaml
```

Copy it to your project:

```bash
cp $(python -c "import kyvvu.templates; print(kyvvu.templates.__path__[0])")/langchain.template.yaml ./my-template.yaml
```

## Loading your custom template

Two ways:

### Environment variable (recommended for deployment)

```bash
export KV_TEMPLATE_LOCATION=./my-template.yaml
```

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

### Constructor parameter (explicit)

```python
from kyvvu_engine.templates import BehaviorTemplate
from kyvvu.integrations.langchain import KyvvuLangChainHandler

template = BehaviorTemplate.from_path("./my-template.yaml")
handler = KyvvuLangChainHandler(kv, template=template)
```

## Template YAML format

```yaml
name: my-company-langchain
version: "1.0"
description: |
  Custom template for Acme Corp's LangChain agents.

rules:
  - id: rule_identifier
    description: Human-readable explanation
    match:
      event: "on_tool_start"          # exact match on event name
      name_pattern: "^fetch_.*"       # regex match on tool/step name
    behavior:
      step_type: "step.resource"      # atomic behavior type
      verb: "GET"                     # HTTP-style verb
      step_name: "{{ name }}"         # Jinja2 template — inserts the tool name
      properties:                     # custom metadata for policy evaluation
        target:
          resource_type: "database"
          contains_pii: true
```

### 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`, `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 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:

```yaml
rules:
  # ... task lifecycle rules ...

  # Custom: PII tools get special properties
  - id: pii_tool
    description: Tools that handle personally identifiable information
    match:
      event: "on_tool_start"
      name_pattern: "^(fetch_customer|lookup_user|get_profile|search_patients).*"
    behavior:
      step_type: "step.resource"
      verb: "GET"
      step_name: "{{ name }}"
      properties:
        target:
          resource_type: "tool"
          contains_pii: true

  # ... rest of the default rules ...
```

Now you can write a policy that blocks PII tools without a preceding gate.

### Classify a tool as destructive (DELETE)

```yaml
  - id: archive_tool
    description: archive_record is a destructive operation
    match:
      event: "on_tool_start"
      name_pattern: "^archive_record$"
    behavior:
      step_type: "step.resource"
      verb: "DELETE"
      step_name: "{{ name }}"
      properties:
        target:
          resource_type: "tool"
          destructive: true
```

### Tag all database tools

```yaml
  - id: db_tools
    description: All db_ prefixed tools are database operations
    match:
      event: "on_tool_start"
      name_pattern: "^db_.*"
    behavior:
      step_type: "step.resource"
      verb: "{{ name | regex_replace('^db_write.*', 'POST') | regex_replace('^db_read.*', 'GET') | default('GET') }}"
      step_name: "{{ name }}"
      properties:
        target:
          resource_type: "database"
```

## 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:

```
kyvvu: matched context {'event': 'on_tool_start', 'name': 'fetch_customer'} -> {'step_type': 'step.resource', 'verb': 'GET', 'step_name': 'fetch_customer', 'properties': {'target': {'contains_pii': true}}}
```

If you see `kyvvu: unmatched event context`, your template is missing a rule for that event type.

***

## Next steps

* [LangChain / LangGraph Integration](/integrations/langchain.md) — the built-in handler these templates work with
* [Atomic Behaviours](/core-concepts/behaviours.md) — the full vocabulary of behavior types
* [Writing a New Integration](/integrations/custom.md) — for non-LangChain frameworks that need a Python adapter
