> 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/authoring.md).

# Template Authoring Guide

**What you'll learn:** How to create a custom behavioral template that maps your framework's events to Kyvvu's 12 atomic behavior types.

***

## When you need a custom template

The SDK ships four built-in templates:

* **`decorator`** -- for Python agents using `@kv.step` decorators.
* **`langchain`** -- for LangChain agents using the callback handler.
* **`langgraph`** -- for LangGraph graph executions.
* **`crewai`** -- for CrewAI agents.

You need a custom template when you're integrating a framework that none of the built-ins cover and isn't using `@kv.step` decorators. Examples: a custom HTTP-based agent, a Microsoft AutoGen agent, or any framework that emits its own event types. The template **engine** (loading, matching, interpolation) lives in `kyvvu_engine.templates`; the YAML files live with their integration.

## Template YAML anatomy

A template is a YAML file with three top-level keys and an ordered list of rules. Here's an annotated excerpt from the built-in `decorator` template:

```yaml
name: decorator                    # Template identifier
version: "1.0"                     # Version string (for your own tracking)
description: |                     # Optional human-readable description
  Maps Python decorator annotations to v0.05 Kyvvu atomic behaviors.

rules:                             # Ordered list of matching rules

  - id: model_call                 # Unique rule identifier
    description: |                 # What this rule matches (for documentation)
      AI model inference: LLM completion, chat, embedding, vision, etc.
    match:                         # Conditions -- all must be true for this rule to fire
      step_type: "step.model"
    behavior:                      # Output fields when the rule matches
      step_type: "step.model"
      verb: "POST"
      step_name: "{{ function_name }}"
      properties:
        model:
          name: "{{ kwargs.model | default('unknown') }}"
          provider: "{{ kwargs.provider | default('unknown') }}"

  - id: unknown                    # Catch-all fallback rule (should be last)
    match:
      step_type: "step.unknown"
    behavior:
      step_type: "step.unknown"
      step_name: "{{ function_name }}"
```

Each rule has:

| Key           | Required    | Purpose                                                           |
| ------------- | ----------- | ----------------------------------------------------------------- |
| `id`          | Recommended | Unique identifier for the rule (for debugging and documentation). |
| `description` | No          | Human-readable description.                                       |
| `match`       | Yes         | Conditions dict. All conditions must pass (implicit AND).         |
| `behavior`    | Yes         | Output dict with `step_type`, `verb`, `step_name`, `properties`.  |

> **Note:** Do not set `scope` in a template. The behavior's scope is derived automatically from `step_type` at emit time (`task.*` → `task`, everything else → `step`) by the decorator and every callback adapter, so a `scope:` key in the YAML is ignored.

## The context dict

When your integration fires, it builds a **context dict** and passes it to `template.match(context)`. The template's rules match against this context.

The context keys depend on your integration. Here are the two built-in examples:

**Decorator context keys:**

```python
{
    "step_type": "step.resource",       # From @kv.step(step_type=...)
    "verb": "GET",                       # From @kv.step(verb=...)
    "function_name": "fetch_customer",   # The decorated function's __name__
    "kwargs": {"url": "https://...", "customer_id": "C-123"},  # Call arguments
}
```

**LangChain context keys:**

```python
{
    "event": "on_llm_start",            # LangChain callback name
    "name": "ChatOpenAI",               # Run name or model name
    "serialized": {"name": "gpt-4o"},   # LangChain serialized object
    "inputs": {...},                     # Chain inputs (on_chain_start)
}
```

**Designing context keys for a new framework:**

1. Identify what events your framework emits (e.g. `http_request`, `llm_call`, `tool_invoke`).
2. Choose a primary discriminator key (e.g. `event` or `event_type`).
3. Include any metadata your rules need to distinguish sub-types (e.g. HTTP method, model name, tool name).
4. Include data you want to extract into properties (e.g. URL, model parameters).

## Match semantics

Rules are evaluated **top to bottom** in list order. The first rule whose `match` block fully matches the context wins. Evaluation stops immediately -- no further rules are checked.

### Exact string matching

Each key in `match` is compared to the same key in the context dict. All keys must match (implicit AND):

```yaml
match:
  event: "on_tool_start"     # context["event"] == "on_tool_start"
  verb: "GET"                # AND context["verb"] == "GET"
```

### Nested keys (dot-path)

Use dot notation to match nested dict values:

```yaml
match:
  serialized.name: "gpt-4o"  # context["serialized"]["name"] == "gpt-4o"
```

### Regex patterns

Two special condition keys support regex matching:

* `name_pattern` -- regex match against `context["name"]` (case-insensitive)
* `function_name_pattern` -- regex match against `context["function_name"]` (case-insensitive)

```yaml
match:
  event: "on_tool_start"
  name_pattern: "^(search|fetch|read|get).*"   # Tool name starts with a read verb
```

### Why rule order matters

Because of first-match-wins, **specific rules must come before general ones**. For example, in the LangChain template, tool rules are ordered: read-pattern tools, then write-pattern tools, then the catch-all tool default. If the catch-all were first, it would match every tool and the specific patterns would never fire.

### The fallback/default rule

A rule with an empty `match` block matches everything:

```yaml
- id: fallback
  match: {}
  behavior:
    step_type: "step.unknown"
    step_name: "unrecognized"
```

Every template should end with a fallback rule so that unrecognized events produce a `step.unknown` behavior instead of returning `None` (no match).

## Property extraction

String values in the `behavior` block support `{{ }}` interpolation from the context dict.

### Simple lookup

```yaml
step_name: "{{ function_name }}"
```

Resolves `context["function_name"]`. If the key is missing, produces an empty string.

### Default values

```yaml
name: "{{ kwargs.model | default('unknown') }}"
```

If `context["kwargs"]["model"]` is missing or `None`, produces `"unknown"`.

### Nested paths

```yaml
host: "{{ kwargs.url | default('') }}"
```

Traverses `context["kwargs"]["url"]` using dot-path notation.

### Mapping to Kyvvu's property schema

Properties are grouped by purpose. The standard groups (from the [Behaviours reference](/core-concepts/behaviours.md)) are:

| Group     | Used with            | Example keys                                                  |
| --------- | -------------------- | ------------------------------------------------------------- |
| `target`  | `step.resource`      | `host`, `resource_type`, `system`, `table`, `domain`, `trust` |
| `model`   | `step.model`         | `name`, `provider`, `temperature`, `parameters`               |
| `guard`   | `step.gate`          | `check_type`, `result`                                        |
| `message` | `step.message`       | `channel`, `recipient`, `sender`                              |
| `auth`    | `step.credential`    | `credential_name`, `provider`, `scope`                        |
| `data`    | any                  | `classification`, `fields`                                    |
| `exec`    | `step.exec`          | `runtime`, `isolation`, `side_effect_class`                   |
| `usage`   | `step.model` outputs | `prompt_tokens`, `completion_tokens`, `cost_usd`              |

Example property extraction in a rule:

```yaml
behavior:
  step_type: "step.resource"
  verb: "GET"
  step_name: "{{ function_name }}"
  properties:
    target:
      host: "{{ kwargs.url | default('') }}"
      resource_type: "{{ kwargs.resource_type | default('unknown') }}"
    data:
      classification: "{{ kwargs.data_class | default('') }}"
```

## Testing your template

Use `BehaviorTemplate.from_path()` to load your template and `match()` to verify mappings:

```python
from kyvvu_engine.templates import BehaviorTemplate

t = BehaviorTemplate.from_path("my_template.yaml")
result = t.match({"event": "http_request", "method": "GET", "url": "https://api.example.com"})
print(result)
# {'step_type': 'step.resource', 'verb': 'GET', ...}  # scope is derived later, not in the matched dict
```

Test each event type your framework emits. Test edge cases: missing fields, unknown events, events with `None` values. Verify the fallback rule catches unrecognized events.

## Worked example: HTTP agent template

Suppose you have an agent that emits these events:

| Event                | Data              |
| -------------------- | ----------------- |
| `http_request`       | `method`, `url`   |
| `llm_call`           | `model`, `prompt` |
| `approval_requested` | `approver`        |
| `task_started`       | --                |
| `task_completed`     | --                |

### Step 1: Task lifecycle rules

```yaml
name: http-agent
version: "1.0"
description: |
  Maps HTTP agent events to Kyvvu atomic behaviors.

rules:
  - id: task_start
    description: Agent begins a new task
    match:
      event: "task_started"
    behavior:
      step_type: "task.start"
      step_name: "task_started"

  - id: task_end
    description: Agent completes a task
    match:
      event: "task_completed"
    behavior:
      step_type: "task.end"
      step_name: "task_completed"
```

### Step 2: LLM call rule

```yaml
  - id: llm_call
    description: Agent calls an LLM
    match:
      event: "llm_call"
    behavior:
      step_type: "step.model"
      verb: "POST"
      step_name: "llm_call"
      properties:
        model:
          name: "{{ model | default('unknown') }}"
```

### Step 3: HTTP request rule

```yaml
  - id: http_request
    description: Agent makes an HTTP request
    match:
      event: "http_request"
    behavior:
      step_type: "step.resource"
      verb: "{{ method | default('GET') }}"
      step_name: "http_request"
      properties:
        target:
          host: "{{ url | default('') }}"
```

### Step 4: Approval gate

```yaml
  - id: approval
    description: Agent requests human approval
    match:
      event: "approval_requested"
    behavior:
      step_type: "step.gate"
      step_name: "approval_requested"
      properties:
        guard:
          check_type: "human_approval"
          result: "{{ result | default('pending') }}"
```

### Step 5: Fallback rule

```yaml
  - id: fallback
    description: Catch-all for unrecognized events
    match: {}
    behavior:
      step_type: "step.unknown"
      step_name: "{{ event | default('unknown') }}"
```

### Step 6: Test it

```python
from kyvvu_engine.templates import BehaviorTemplate

t = BehaviorTemplate.from_path("http_agent.template.yaml")

# Task lifecycle
r = t.match({"event": "task_started"})
assert r["step_type"] == "task.start"

# LLM call
r = t.match({"event": "llm_call", "model": "gpt-4o", "prompt": "Hello"})
assert r["step_type"] == "step.model"
assert r["properties"]["model"]["name"] == "gpt-4o"

# HTTP request
r = t.match({"event": "http_request", "method": "GET", "url": "https://api.example.com"})
assert r["step_type"] == "step.resource"
assert r["verb"] == "GET"
assert r["properties"]["target"]["host"] == "https://api.example.com"

# Approval gate
r = t.match({"event": "approval_requested", "approver": "admin@co.com", "result": "pass"})
assert r["step_type"] == "step.gate"
assert r["properties"]["guard"]["check_type"] == "human_approval"

# Unknown event falls to default
r = t.match({"event": "something_unexpected"})
assert r["step_type"] == "step.unknown"
```

***

## Next steps

* [Templates overview](/core-concepts/templates.md) -- deep-merge semantics and built-in template details
* [Atomic Behaviours](/core-concepts/behaviours.md) -- the 12 behavior types and property groups
* [Writing a New Integration](/integrations/custom.md) -- the `FrameworkAdapter` base class for hooking into frameworks
* [Pattern templates](https://github.com/Kyvvu/platform/blob/main/kyvvu-sdk/kyvvu/templates/patterns/README.md) -- reference snippets for common agent architectures (RAG, ReAct, human-in-the-loop, multi-agent)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://docs.kyvvu.com/integrations/authoring.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
