Tool Specification: YAML Schema
Complete guide to writing Matimo tools in YAML.
Overview
Section titled “Overview”Every Matimo tool is defined in a YAML file with a standardized schema. Tools define:
- Metadata: Name, version, description
- Parameters: What inputs the tool accepts
- Execution: How the tool runs (HTTP, command, function)
- Output: What the tool returns
- Authentication: How to authenticate (if needed)
- Error Handling: Retry and recovery logic
Basic Structure
Section titled “Basic Structure”name: tool-namedescription: Brief description of what the tool doesversion: '1.0.0'
parameters: # Define input parameters here
execution: # Define how to execute the tool
output_schema: # Define the output format
authentication: # Optional # Define authentication if needed
requires_approval: true # Optional, see Governance Fieldsrisk: medium # Optional (required for function tools)
error_handling: # Optional # Retry settings (accepted, not yet applied)Metadata
Section titled “Metadata”Unique tool identifier.
name: slack_send_channel_message # newer packages: snake_casename: github-create-issue # older packages: kebab-casename: calculatorRules:
- Lowercase, no spaces
- Globally unique among the tools a
MatimoInstanceloads - New tools use
snake_case. The GitHub, Gmail, HubSpot, Mailchimp, Twilio and Postgres packages andslack-send-messagepredate that and use kebab-case. Neither style is enforced by the loader, so keep the convention of the package you’re adding to
description
Section titled “description”What the tool does. One sentence.
description: Create a new GitHub issue in a repositorydescription: Send a message to a Slack channeldescription: Perform basic math calculationsversion
Section titled “version”Semantic versioning: MAJOR.MINOR.PATCH
version: "1.0.0"version: "2.1.3"Parameters
Section titled “Parameters”Define what inputs the tool accepts.
Basic Structure
Section titled “Basic Structure”parameters: param_name: type: string|number|boolean|object|array description: What this parameter does required: true|falseParameter Properties
Section titled “Parameter Properties”type (required)
Section titled “type (required)”Parameter data type.
type: string # Text inputtype: number # Integer or floattype: boolean # True/falsetype: object # JSON objecttype: array # Array of itemsdescription (required)
Section titled “description (required)”What this parameter does.
description: GitHub repository in owner/repo formatdescription: Number of items to fetchdescription: Enable verbose loggingrequired (optional)
Section titled “required (optional)”Whether the parameter is mandatory. Defaults to optional, so say required: true for anything the tool cannot run without.
required: true # Must be providedrequired: false # Optionaldefault (optional)
Section titled “default (optional)”Default value if not provided.
default: 10default: "main"default: falseenum (optional)
Section titled “enum (optional)”List of allowed values.
enum: - add - subtract - multiply - divideexamples, items, properties (optional)
Section titled “examples, items, properties (optional)”examples lists sample values. items describes the element type of an array parameter, and properties describes the fields of an object parameter. Both nest.
No min, max or pattern keys
Section titled “No min, max or pattern keys”There is no validation: block. A key Matimo doesn’t know, such as minLength, min or pattern, is ignored when the YAML loads, so it enforces nothing. State limits in the description instead, and enforce them in the API the tool calls.
MatimoInstance.execute() does not check arguments against type, required or enum either (only a missing {placeholder} in an HTTP URL is rejected). Those fields describe the tool to the model: the LangChain adapter turns them into a Zod schema, and the MCP server publishes them as the tool’s inputSchema. default is applied only by the LangChain adapter.
Examples
Section titled “Examples”String Parameter
Section titled “String Parameter”parameters: message: type: string description: Message to send (1 to 1000 characters) required: trueNumber Parameter with a Default
Section titled “Number Parameter with a Default”parameters: count: type: number description: Number of items to fetch (1 to 100) required: false default: 10Choice Parameter
Section titled “Choice Parameter”parameters: operation: type: string description: Math operation to perform required: true enum: - add - subtract - multiply - divideObject Parameter
Section titled “Object Parameter”parameters: config: type: object description: Configuration object required: true properties: timeout: type: number retries: type: numberExecution
Section titled “Execution”Define how the tool runs.
Type: Command
Section titled “Type: Command”Execute shell commands.
execution: type: command command: node args: - script.js - '{param1}' - '{param2}' timeout: 5000Fields:
command(string, required), The executable. It cannot contain{placeholders}; onlyargsare templatedargs(array, optional), Command arguments with parameter substitutiontimeout(number, optional, default: 30000), Timeout in millisecondscwd,shell(optional), Accepted by the schema. Python appliescwd; TypeScript ignores both and runs without a shell
The child process gets the parent’s environment plus any per-call credentials. Python also accepts an env map under execution and adds it; TypeScript’s schema drops env, so don’t rely on it in a tool meant for both SDKs.
A command tool asks a human before every call unless it declares requires_approval: false, and agents cannot create one.
Parameter Substitution:
Use {param_name} to reference parameters.
args: - '--operation={operation}' - '{a}' - '{b}'When executed with { operation: 'add', a: 5, b: 3 }, becomes:
--operation=add 5 3Type: HTTP
Section titled “Type: HTTP”Make HTTP requests.
execution: type: http method: POST url: 'https://api.example.com/endpoint' headers: Content-Type: application/json Authorization: 'Bearer {MATIMO_API_KEY}' body: name: '{name}' timeout: 10000Fields:
method(string, required), HTTP method: GET, POST, PUT, DELETE, PATCHurl(string, required), API endpoint URL with parameter substitutionheaders(object, optional), HTTP headersbody(any, optional), Request body, with parameter substitutionquery_params(object, optional), Query string parametersparameter_encoding(array, optional), See HTTP Parameter Embeddingtimeout(number, optional), Timeout in milliseconds. Python defaults to 30000; TypeScript sets no timeout unless you give one, so set it explicitly
Authentication goes in the top-level authentication block (see Authentication), not under execution.
URL Templating:
url: 'https://api.github.com/repos/{owner}/{repo}/issues'When executed with { owner: 'tallclub', repo: 'matimo' }:
https://api.github.com/repos/tallclub/matimo/issuesType: Function
Section titled “Type: Function”Execute a JavaScript/TypeScript module exported as the tool’s default function. The code field is a path (relative to the tool’s YAML file) to the implementation module.
execution: type: function code: './my_tool.ts' timeout: 30000The implementation module must export a default async function. Its optional second argument carries the call’s per-call credentials and the caller’s policy context, both supplied by the host (execute(..., { credentials, context })), never by the agent:
import type { FunctionToolContext } from '@matimo/core';
export default async function myTool( params: Record<string, unknown>, context?: FunctionToolContext // { credentials?, policyContext?: { agentId, roles, environment } }): Promise<unknown> { if (!context?.policyContext?.roles?.includes('admin')) { return { success: false, error: 'admin only' }; } return { result: params.value };}The Python SDK runs my_tool.py next to the YAML (same name, .py suffix). Its run() may be sync or async, and receives a FunctionToolContext when it accepts a second argument:
from matimo import FunctionToolContext
def run(params: dict, context: FunctionToolContext | None = None) -> dict: caller = context.policy_context if context else None return {"result": params.get("value"), "agent": caller.agent_id if caller else None}Fields:
code(string, required), Relative path to the implementation module (.tsor.js; Python uses the sibling.py)timeout(number, optional), Execution timeout in milliseconds
Every function tool must declare risk:, it is the risk the policy engine uses when the tool runs, since its code can do anything. pnpm validate-tools and make validate-tools reject a function tool without one. See Governance Fields.
Trust model, IMPORTANT:
execution.type: function is blocked for agent-created tools (untrusted source). Agents cannot propose tools with this execution type because it allows arbitrary code execution. Only developer-authored tools in trustedPaths (installed @matimo/* packages or explicit file paths) may use type: function.
In TypeScript this is a hard block, the policy engine rejects any untrusted tool with execution.type: function regardless of policy configuration. The Python SDK rejects it unless the policy sets allow_function_tools.
If you are building meta-tools (like the built-in matimo_approve_tool), use type: function freely, they live in trusted paths.
Governance Fields
Section titled “Governance Fields”These fields decide how the policy engine treats a tool. All are optional except risk on function tools.
requires_approval: true # ask a human before every callrisk: high # low | medium | high | criticalstatus: approved # draft | approved | deprecateddeprecated: falsedeprecation_message: 'Use slack_send_channel_message instead'| Field | Effect |
|---|---|
requires_approval |
true: every call waits for an approval callback (Approval System). Unset: HTTP DELETE and type: command tools still ask (the 0.2.0 secure default); everything else doesn’t. false: opts a DELETE or command tool out of that default, the destructive-keyword scan of sql/command arguments still applies. HTTP DELETE tools in this repo must say requires_approval: true; the validator enforces it |
risk |
Raises the automatically computed risk (GET low; POST/PUT/PATCH medium; DELETE, command, or requires_approval high), never lowers it. For function tools it is the risk, and is required |
status |
draft tools never run in production and run elsewhere only for an admin caller; deprecated tools never run. Agent-created tools start as draft |
deprecated / deprecation_message |
Same as status: deprecated; the message is returned to the caller |
Risk feeds HITL quarantine (enableHITL + hitlMinRiskLevel) and appears on every tool:executed event. See Policy and Lifecycle.
Output Schema
Section titled “Output Schema”Define what the tool returns.
output_schema: type: object properties: id: type: number name: type: string created_at: type: string required: - id - nameSupported Types
Section titled “Supported Types”type: object # JSON objecttype: array # Array of itemstype: string # Texttype: number # Integer or floattype: boolean # True/falseObject Schema
Section titled “Object Schema”type: objectproperties: field_name: type: string description: Field description field_count: type: numberrequired: - field_nameArray Schema
Section titled “Array Schema”type: arrayitems: type: object properties: id: type: number name: type: stringExamples
Section titled “Examples”Simple Object
Section titled “Simple Object”output_schema: type: object properties: result: type: number timestamp: type: string required: - resultArray of Objects
Section titled “Array of Objects”output_schema: type: array items: type: object properties: id: type: number title: type: string author: type: stringNested Objects
Section titled “Nested Objects”output_schema: type: object properties: user: type: object properties: id: type: number name: type: string email: type: string status: type: string required: - user - statusmax_response_size
Section titled “max_response_size”Optional. Caps how many bytes of this tool’s raw result Matimo will return before truncating it, overriding the instance-wide default.
output_schema: type: array items: type: object max_response_size: 1048576 # 1 MB, in bytesEvery tool call passes through a response-size guardrail in execute(), regardless of whether it’s invoked directly, via LangChain/CrewAI, or via MCP, so this applies uniformly across every integration. Precedence, highest first:
- This tool’s own
output_schema.max_response_size - The instance-level default passed to
MatimoInstance.init()(defaultMaxResponseSizein TS,default_max_response_sizein Python) - A built-in 256 KB (262,144 byte) default applied to every tool that sets neither of the above
When a result exceeds the effective cap, it’s truncated rather than rejected:
- Arrays are sliced to fit the byte budget, with a sentinel element appended:
"...truncated, N of M items shown". - Long strings are sliced with an inline
"...truncated, N of M characters shown"marker. - Large objects are truncated per-field (smaller fields like
statusCode/headersare kept intact; the largest field absorbs the cut), and the outermost object gets an additive_truncated: truefield, existing keys are never renamed or removed.
See Tool Execution Flow for where this fits in the request lifecycle, and Error Codes Reference for how a truncated result differs from an error response (it’s never an error, the result is still returned, just capped).
Authentication
Section titled “Authentication”Describe how the tool authenticates (optional).
authentication: type: none|api_key|basic|bearer|oauth2|custom location: header|query|body name: header_name provider: provider_name # oauth2 only required: true username_env: ENV_NAME # basic only password_env: ENV_NAME # basic onlyThere is no secret_env_var field. Apart from basic, the authentication block describes the scheme and is not what injects the secret. The secret reaches the request through a {PLACEHOLDER} in headers, query_params, url or body:
execution: type: http method: GET url: 'https://api.example.com/items' headers: Authorization: 'Bearer {EXAMPLE_API_KEY}'
authentication: type: bearer location: header name: AuthorizationOnly a placeholder whose name contains token, key, secret, password, credential, auth or bearer is treated as a credential, so name yours that way. At call time Matimo fills {EXAMPLE_API_KEY} from the first of these that is set (see Authentication):
- The call’s
credentials:EXAMPLE_API_KEY, thenMATIMO_EXAMPLE_API_KEY - The environment variable
MATIMO_EXAMPLE_API_KEY - The environment variable
EXAMPLE_API_KEY
Type: Basic Auth
Section titled “Type: Basic Auth”basic is the one scheme the HTTP executor handles itself. Name the two variables that hold the username and password, and it builds the Authorization: Basic ... header:
authentication: type: basic username_env: TWILIO_ACCOUNT_SID password_env: TWILIO_AUTH_TOKENIf either variable is unset (and not passed in credentials) the call fails with an error naming both.
Type: OAuth2
Section titled “Type: OAuth2”authentication: type: oauth2 provider: githubprovider names a provider definition that holds the authorization and token URLs. Matimo does not run the browser flow or refresh tokens for you: your application obtains the token (see OAuth2 and credentials) and supplies it as the placeholder value.
Error Handling
Section titled “Error Handling”Not applied yet.
error_handlingis validated and kept on the tool definition, but neither SDK’s executors read it today: a failed call is not retried. Retries with these settings are planned. Errors carrydetails.retryable(set for timeouts, 429 and 5xx responses) so a caller can retry itself.
Retry settings (optional).
error_handling: retry: 3 backoff_type: exponential|linear|fixed initial_delay_ms: 1000 max_delay_ms: 30000Fields:
retry(number), Number of retry attemptsbackoff_type(string),exponential,linearorfixedinitial_delay_ms(number), Initial delay between retriesmax_delay_ms(number), Maximum delay between retries
Backoff Strategies
Section titled “Backoff Strategies”exponential
Section titled “exponential”Wait time = initial_delay × (2 ^ attempt_number)
error_handling: retry: 3 backoff_type: exponential initial_delay_ms: 1000 max_delay_ms: 30000Delays: 1s, 2s, 4s, 8s (capped at 30s)
linear
Section titled “linear”Wait time = initial_delay × (attempt_number + 1)
backoff_type: linearinitial_delay_ms: 1000Delays: 1s, 2s, 3s, 4s
Wait time = initial_delay
backoff_type: fixedinitial_delay_ms: 2000Delays: 2s, 2s, 2s, 2s
Complete Examples
Section titled “Complete Examples”Add Two Numbers (command tool)
Section titled “Add Two Numbers (command tool)”A minimal type: command tool. Arguments are passed to the process as separate args entries, never through a shell, so a parameter value cannot inject extra commands. {placeholder} only substitutes values; it does not evaluate expressions.
name: add_numbersdescription: Add two numbersversion: '1.0.0'risk: lowrequires_approval: false # command tools ask a human before every call unless they opt out
parameters: a: type: number description: First number required: true b: type: number description: Second number required: true
execution: type: command command: node args: - -e - 'console.log(JSON.stringify({ result: Number(process.argv[1]) + Number(process.argv[2]) }))' - '{a}' - '{b}'
output_schema: type: object properties: result: type: numberThe built-in calculator is a type: function tool with a sandboxed expression parser (typescript/packages/core/tools/calculator/definition.yaml). It is a better model for anything beyond arithmetic.
GitHub Create Issue
Section titled “GitHub Create Issue”From typescript/packages/github/tools/create-issue/definition.yaml (abridged):
name: github-create-issuedescription: Create a new issue in a repositoryversion: '1.0.0'requires_approval: true
parameters: owner: type: string required: true description: Repository owner username or organization repo: type: string required: true description: Repository name title: type: string required: true description: Issue title body: type: string required: false description: Issue description (markdown supported) labels: type: array required: false description: Array of label names
execution: type: http method: POST url: 'https://api.github.com/repos/{owner}/{repo}/issues' headers: Accept: application/vnd.github+json Authorization: 'Bearer {GITHUB_TOKEN}' X-GitHub-Api-Version: '2022-11-28' Content-Type: application/json body: title: '{title}' body: '{body}' labels: '{labels}' # an array parameter is sent as a JSON array timeout: 15000
authentication: type: bearer location: header
output_schema: type: object properties: number: type: number description: Issue number in repository title: type: string html_url: type: stringSlack Send Message
Section titled “Slack Send Message”From typescript/packages/slack/tools/slack_send_channel_message/definition.yaml:
name: slack_send_channel_messagedescription: Post a message (text, markdown, blocks) to a public/private Slack channel.version: '1.0.0'parameters: channel: type: string required: true description: Channel ID or name to post the message to text: type: string required: false description: Plain-text message (optional if blocks provided, recommended as fallback for accessibility)execution: type: http method: POST url: 'https://slack.com/api/chat.postMessage' headers: Authorization: 'Bearer {SLACK_BOT_TOKEN}' Content-Type: application/json body: channel: '{channel}' text: '{text}' timeout: 15000authentication: type: api_key location: header name: Authorizationnotes: env: SLACK_BOT_TOKENBest Practices
Section titled “Best Practices”- Naming: Lowercase and globally unique; new tools use
snake_case(slack_send_channel_message), though older packages use kebab-case - Description: Clear, one sentence explaining purpose
- Parameters: A description on every parameter; use
enumfor fixed values and state limits in the description (there are no min/max/pattern keys) - Output Schema: Document the response fields the caller needs
- Authentication:
{UPPER_CASE}placeholders filled from the environment; never hardcode a secret - Governance:
requires_approval: trueon anything destructive, andrisk:on function tools - Timeout: Set appropriate timeouts (avoid infinite hangs)
- Testing: Include examples of real-world usage
File Organization
Section titled “File Organization”Store tools in provider directories. Each tool can be a single file or a subdirectory with its own definition.yaml:
tools/├── provider-name/│ ├── definition.yaml # Single tool per provider│ └── ...├── multi-tool-provider/│ ├── tool-1/│ │ └── definition.yaml # Multi-tool provider│ └── tool-2/│ └── definition.yaml└── examples/ └── example-tool.yamlExample:
typescript/packages/gmail/├── definition.yaml # Provider manifest (OAuth2 endpoints), not a tool└── tools/ ├── send-email/ │ └── definition.yaml ├── create-draft/ │ └── definition.yaml └── list-messages/ └── definition.yaml
typescript/packages/core/tools/└── calculator/ ├── definition.yaml └── calculator.ts # Implementation for a `type: function` toolNaming conventions:
- Tool subdirectories hold one tool each and are named after it (
send-email,slack_send_channel_message) - Files: Always
definition.yamlfor tool definitions - A
definition.yamlat a provider’s root is the provider manifest, not a tool, and is not counted among its tools
See Also
Section titled “See Also”- Quick Start: Get started in 5 minutes
- API Reference: Complete SDK documentation
- Decorator Guide: Use decorators
- CONTRIBUTING.md: Development guide