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

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

```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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.kyvvu.com/integrations/custom-template.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
