LeafMesh Agent Configuration — Complete Field Reference
This document lists every configuration field, its type, default value, and accepted values. Use this to build frontend forms, dropdowns, and validation.
Table of Contents
- Agent Types
- AgentConfig — Core Fields (All Types)
- LLM Agent Fields
- Human Agent Fields
- External Agent Fields
- Programmatic Agent Fields
- WebhookConfig
- ChannelConfig
- Memory Config
- EscalationConfig
- EscalationTarget
- BrokerConfig
- EventListener
- Top-Level Config (LeafMeshConfig)
- ManagerConfig
- MeshConfig & Cloud Providers
- Store (.env, not YAML)
- EvolutionConfig
- DataStructure
- Entry Points
- Validation Rules
- Field Applicability by Agent Type
Agent Types
| Value | Description |
|---|---|
llm | LLM-powered agent (default). Executes via OpenAI, Claude, Bedrock, Vertex, or Foundry. |
human | Human operator agent. Routes to a person via API, webhook, or channel. |
programmatic | Python function with business logic. No LLM calls. |
external | Delegates to an external framework (CrewAI, LangGraph, AutoGen, etc.). |
AgentConfig — Core Fields (All Types)
These fields apply to every agent regardless of agent_type.
| Field | Type | Default | Accepted Values | Required | Description |
|---|---|---|---|---|---|
name | string | — | any string | yes | Unique agent name within the mesh |
description | string | null | any string | no | Agent description and purpose |
agent_type | string | "llm" | llm, human, programmatic, external | no | Agent execution type (see note below) |
communication_type | string | "dual" | dual, chain, execute | no | How agent communicates with the mesh |
response_overrides | list or dict | null | list of {caller, type} OR dict {caller_name: type} | no | Per-caller override of communication_type. Falls back to the default for any caller not listed and for external entry points. See [Per-caller Response Overrides](#per-caller-response-overrides). |
scaling | object | — | runs, copies | no | How the agent runs in parallel (2.4.484+), all four agent types; `copies` on a human seat is refused at load (2.4.485), `runs.max` is fine. Replaces parallel, max_concurrent, instances and instances_handoff, which are refused at load with the scaling: lines to write. runs.max: at most N of this agent's jobs at once across ALL pods; extra jobs wait their turn; unset = no cap. copies works on ONE job: mode fixed (default) or auto; count (fixed, default 1, 1–50); max (auto ceiling, default 10, 1–50); work (auto: the input field holding the list) OR claim_tool (auto: the agent's own tool that hands out the next item) — exactly one in auto, neither in fixed; handoff last (default: one combined output — lists joined, numbers summed, other fields from the final copy — handed on once) or each. Auto starts a copy only when there is an item for it; copies take more with the built-in next_work_item tool ({item} or {done: true}). Each copy's input carries _instance: {id, of} (auto: of = "auto", plus _work: {item, field}); the combined output carries _instances: {count, results, failed}. See [Agent Configuration → scaling](agent-config#scaling) |
notify | dict | null | {webhook, when} | no | 2.4.486+. Post the agent's output to a webhook made on Tools → Webhooks when it hands its work on or fails. webhook: the webhook's name (an address is refused). when: done (default), failed or both. Only these two keys. See [Tell your systems when work is done](../tools/api-tools#tell-your-systems-when-work-is-done) |
tenant_credentials_only | bool | false | true, false | no | 2.4.488+. A run carrying a tenant id uses only that tenant's own credentials: its model key and the record this agent's connector names (its connection). If the tenant has not connected one, the run stops with *this tenant has not connected <record name>* and never falls back to your key. Runs without a tenant id are unchanged. See [Bring Your Own Vault → Tenants' own model keys](../integrations/bring-your-own-vault). |
wake_up | string or list | null | cron string; OR (2.4.107+) a list of cron strings; OR a list of {cron, input} entries | no | Scheduled wake-up(s). Multiple schedules per agent are supported; each entry may carry its own input dict merged into that wake's payload. From 2.4.492 a wake-up added, changed, renamed or removed in Studio or through the config API applies at once on every running pod, with no restart |
wake_up_input | dict | null | any JSON object | no | 2.4.107+ — declared input template for scheduled wake-ups. Merged as the BASE of every wake payload (per-schedule input overrides it; trigger markers wake_up_trigger/trigger_time always win). Cron-woken agents receive real input instead of a bare trigger |
yields | dict | {} | key: field name, value: type string or nested object | no | Output schema — what the agent produces. Nested objects: {type: object, fields: {...}}; arrays: {type: array, items: ...}. Any declaration may add nullable: true (2.4.104) to accept an explicit null — declare it instead of leaving legitimately-null fields undeclared |
inputs | dict | {} | key: field name, value: type string or nested object | no | Input schema — what agent expects |
can_call | list | [] | list of {"agent": "name"} or {"agent": "name", "condition": "expr"} | no | Agents this agent can invoke |
listen_events | list | [] | list of [EventListener](#eventlistener) entries | no | External event sources (Kafka/SQS/MQTT/Redis Streams/IMAP) that trigger this agent. Each entry references a broker by name from the top-level brokers: block. |
narration | string | null | any string (multiline supported) | no | Plain-English routing hints for the Manager — evaluated when conditions don't cover everything (see [Narration Routing](#narration-routing)) |
wait_for | string or list | [] | agent names or expression string | no | Fan-in/join condition |
wait_for_timeout | int | 60 | 1 – unlimited (seconds) | no | Hard timeout for fan-in |
auto_store_response | bool | true | true, false | no | Auto-store responses in Redis |
auto_store_yields | bool | true | true, false | no | Auto-store yields in Redis |
memory | bool or dict | false | true, false, or memory config dict | no | Agent memory (LLM agents) — see [Memory Config](#memory-config) |
memory_limit | int | 10 | 1 – 100 | no | Legacy: max recent feed posts (use memory.limit instead) |
knowledge | bool or dict | false | false, or {serviceName, enabled, groupName} | no | Knowledge/RAG (LLM agents) — see [Knowledge Config](#knowledge-config) |
enforce_yields | bool | false | true, false | no | Strictly validate this agent's output against the declared yields: schema. false (default) fills missing keys with type defaults and logs warnings; true triggers a Manager-driven retry up to enforce_yields_retry times, then escalates. See [Yields Enforcement](#yields-enforcement). |
enforce_yields_retry | int | 0 | 0 – unlimited | no | Maximum self-correction attempts when enforce_yields: true. 0 fails on first contract violation. Each retry passes the previous output + validation errors as feedback so LLM/external/human/programmatic agents can self-correct. Honored by every agent type. |
receive_conversation_history | bool | false | true, false | no | Opt-in for non-LLM agents (programmatic / human / external) to receive prior conversation turns in their payload. LLM agents already see history via the prompt; this flag has no effect on them. When true: programmatic agents read it from context["_conversation_history"], external connectors get it on input_data._conversation_history AND context._conversation_history, human webhooks include it in the body. See [Conversation History Opt-In](#conversation-history-opt-in). |
history_limit | int | 20 | 1 – 200 | no | Max prior turns delivered when receive_conversation_history: true. Ignored when the flag is off. Note: human agents that haven't opted in still use the legacy 5-turn fallback for backwards compatibility — flip the flag to switch to this configurable limit. |
stream_yield | string | null | any yield key from yields: | no | For dual-mode LLM agents, picks which yield field is streamed token-by-token when the caller passes stream=True to mesh_call. If the agent declares exactly one yield key, the SDK auto-picks it and this field can stay unset. See [Streaming Output](#streaming-output). |
Per-caller Response Overrides
communication_type sets one default response behavior for the whole agent — once dual, it replies back to every caller; once chain, never. Real workflows often need different behavior per caller: a sales agent might want to reply back to a customer-facing router (dual) but stay silent when an internal processor calls it (chain).
response_overrides is an optional per-caller override on top of communication_type. Lookup priority at the dual-callback site:
- If
response_overrides[called_by]is set → use that type. - Otherwise → fall back to
communication_type.
External entry points (mesh_call from the outside, event listeners, scheduler wake_up) have no called_by and skip this lookup entirely — they always use the top-level communication_type. The override surface is internal-call-only by design.
List form (mirrors can_call style — recommended for YAML readability):
agents:
sales_agent:
name: "sales_agent"
agent_type: llm
model: gpt-4o-mini
communication_type: chain # default for any caller
response_overrides:
- caller: customer_facing_agent # reply back to this one
type: dual
- caller: escalation_router # reply back to this one too
type: dual
# internal_processor: not listed → falls back to chain → no replyDict form (also accepted, terser):
agents:
sales_agent:
name: "sales_agent"
communication_type: chain
response_overrides:
customer_facing_agent: dual
escalation_router: dualBoth shapes behave identically at runtime.
Studio UI: the agent inspector exposes a "Per-caller overrides" panel under the Communication field. The caller dropdown is restricted to agents whose can_call actually targets this agent — overrides for unrelated agents would be dead config and never fire. When nobody calls this agent AND no overrides are configured, the panel hides entirely.
Available since: SDK v2.2.32 (field + runtime), v2.2.33 (API exposure + Studio panel).
Yields Enforcement
enforce_yields and enforce_yields_retry work together to make yields: an enforceable contract on the producer side, without coupling agents to each other's shapes.
Default behavior (enforce_yields: false) — lenient mode, backwards-compatible:
- Missing yield keys → filled with type defaults (
"",0,[],{},false). - Type mismatches → kept verbatim, WARNING logged.
can_callconditions evaluate on a known shape (no more silent skips on undefined keys).
Strict mode (enforce_yields: true) — for production-critical agents:
- On contract violation, the SDK fires a Manager-driven retry.
- Up to
enforce_yields_retryattempts. Each retry sees the previous (wrong) output + validation errors as rerun context:- LLM agents — the prompt appends a correction note.
- Human agents — outbound payload exposes the rerun context; inbox/channel UI surfaces what's needed.
- External connectors —
data._rerun_contextis added to the workflow payload. - Programmatic agents —
input_data._rerun_contextis available alongside the (possibly auto-corrected) inputs.
- After the retry budget is exhausted, fires
AGENT_ERRORwitherror_type="YieldContractFailure"+retry_exhausted=true. Routes throughmanager.escalation:if configured.
Example:
agents:
client:
name: "client"
agent_type: human
human_interface: webhook
yields:
request_data: object
decision: string
enforce_yields: true # strict
enforce_yields_retry: 3 # 3 self-correction attempts before escalatingProgrammatic agents are retryable too — the Manager's analysis pass can inspect the failure and produce a corrected_input (e.g. fix "ORGANIZATION" → "Organization"). The retry runs the same deterministic function with the corrected input and produces a different result. See Manager — Rerun Flow for the full path.
Dynamic Per-Call Routing (Human Agents)
YAML configures default routing for a human agent — operator_ids: [alice, bob] lists who can see HITL requests, channels.slack.post_channel: "C0SLACK0123" sets the default Slack channel, etc. Real workflows need per-turn dynamism on top: "today's on-call is Alice," "this approval goes to the legal team's WhatsApp," "escalations route to the manager directly."
Routing has two layers. YAML is the contract (sane defaults, channel-scoped operator rosters). Pre-compose is the override (per-turn steering). They work together: pre-compose narrows when your code knows better; YAML catches anything pre-compose didn't set.
Layer 1 — YAML defaults
Operators live inside each channel that knows them. Slack owns Slack IDs, WhatsApp owns phone numbers, email owns addresses — the same logical roster (operator_ids: [alice, bob, carol]) is resolved per-channel via that channel's own operators: map.
agents:
client:
name: "client"
agent_type: "human"
is_human_powered: true
# Default broadcast list — who sees HITL requests when no override.
# This is the LOGICAL roster (same names used across channels below).
operator_ids: [alice, bob, carol]
channels:
slack:
connection: "slack-workspace" # Infrastructure → Messaging
post_channel: "C0DEFAULT" # fallback when nobody overrides
# NEW (v2.2.44): channel-scoped operator roster.
# Lets pre_compose say "_target_operator: alice" — SDK looks up
# alice's Slack ID under this channel's operators map.
operators:
alice: "U0ALICE" # Slack user ID (DM) or channel ID
bob: "U0BOB"
carol: "C0TEAM_CHANNEL" # team channel, not DM
whatsapp:
connection: "whatsapp-business" # Infrastructure → Messaging
post_channel: "+15551111111"
operators:
alice: "+14155551234" # alice's WhatsApp number
bob: "+18005550000"
email:
enabled: true
connection: "ops-email" # Infrastructure → Workspace → Email
from_address: "ops@acme.com"
operators:
alice: "alice@company.com"
bob: "bob@company.com"Why channel-scoped? Each channel naturally owns the recipients it can address. Adding Slack → list its operators once inside the slack block. Removing WhatsApp → all its operator info goes with it (no orphans). You never spread a single recipient ID across two YAML sections.
Layer 2 — Pre-compose runtime override
Pre-compose populates reserved keys in input_data. Underscore prefix marks them as routing metadata (not business fields). Same convention as the existing _operator_ids.
| Key | Used by | Effect |
|---|---|---|
_target_operator (recommended) | All human-agent paths | Logical operator ID. SDK looks up each configured channel's operators map and dispatches to that channel's recipient. Single key drives Slack + WhatsApp + Email + … simultaneously. |
_operator_ids | Default inbox | List override — narrows the HITL broadcast to listed operators. Legacy convention, still works. |
_target_channel_id | Channel adapters | Escape hatch — raw recipient ID in the platform's namespace. Use when the ID comes from somewhere dynamic (a Zapier response, an external API) and you don't want it in YAML. Bypasses operator resolution. |
_target_channel_provider | Channel adapters | Pin outbound to one provider when several configured. Values: slack, discord, telegram, whatsapp, teams, email. |
_target_webhook_url | Webhook fallback | Raw URL override for the address on the webhook_config record. Bypasses operator resolution. |
Layer 2 — Example: daily on-call rotation
from leafmesh import pre_compose
import datetime
@pre_compose(input_data=lambda data, ctx: {
**data,
"_target_operator": _todays_oncall(), # "alice" on Mon, "bob" on Tue, ...
})
async def escalation_agent(input_data, context):
return {
"alert": input_data["incident"],
"severity": input_data["severity"],
}
def _todays_oncall() -> str | None:
"""Map weekday → operator ID. Returns None on weekends → YAML default."""
return {0: "alice", 1: "bob", 2: "carol"}.get(datetime.datetime.now().weekday())If the agent has Slack + WhatsApp + Email configured, the same _target_operator: "alice" routes to all three — alice's Slack DM, alice's WhatsApp number, alice's email — each pulled from its own channel's operators: map. You think in terms of people; each channel resolves to its own recipient namespace.
Resolution order (per outbound dispatch)
For each configured channel:
_target_channel_id(raw override — power user / dynamic ID)_target_operator→channels.<provider>.operators[operator](ergonomic)adapter.get_channel_for_session(session_id)(reply continuity — the channel the inbound message arrived from)adapter.config["post_channel"](YAML default)
For the webhook fallback path:
_target_webhook_url(raw)webhook_config.operators[operator](ergonomic)- The
webhook_configrecord's address (default)
Fail-soft semantics: if _target_operator is set but the operator has no entry under a given channel's operators: map, the SDK logs a WARNING and falls through to (3)/(4) for that channel. Messages are never silently dropped — there's always a YAML default to catch the fallback.
When to use which layer
| Scenario | Use |
|---|---|
| Fixed recipient — always the same person | YAML only (per-channel operators map + operator_ids) |
| Workflow-driven recipient (on-call, round-robin, escalation tier) | YAML + pre_compose (_target_operator) |
| Recipient ID from external system (Zapier, MCP, DB lookup) | YAML + pre_compose (_target_channel_id raw override) |
| Multi-channel rosters (Slack + WhatsApp + Email in parallel) | Per-channel `operators` + pre_compose `_target_operator` — single key drives all transports, each channel resolves its own recipient |
Applies to all human-agent transports
| Transport | Supports _target_operator via | Raw override key |
|---|---|---|
| Default inbox | (narrows broadcast to that operator) | _operator_ids |
| Slack | channels.slack.operators | _target_channel_id + _target_channel_provider: slack |
| Discord | channels.discord.operators | _target_channel_id + _target_channel_provider: discord |
| Telegram | channels.telegram.operators | _target_channel_id + _target_channel_provider: telegram |
channels.whatsapp.operators | _target_channel_id + _target_channel_provider: whatsapp | |
| Teams | channels.teams.operators | _target_channel_id + _target_channel_provider: teams |
channels.email.operators | _target_channel_id + _target_channel_provider: email | |
| Webhook fallback | webhook_config.operators | _target_webhook_url |
Note for LLM, programmatic, external agents: The
_target_*convention applies only to human agents (which actually route messages to people). LLM agents have no recipient. Programmatic / external agents route via their ownconnector_config(Zapier action, MCP tool, framework endpoint) — pre_compose can shape that payload via standard input_data fields, but the operator abstraction is human-specific.
Available since SDK v2.2.44. Backward compatible — agents not using the new keys behave exactly as before.
Conversation History Opt-In
LLM agents automatically receive prior conversation turns in their prompt. Programmatic, human, and external agents do not — they're stateless by design. When you want a non-LLM agent to see history (for e.g. domain-specific summarisation, context-aware retrieval), set the flag and the SDK injects the last history_limit turns at a standard location.
agents:
context_summariser:
name: "context_summariser"
agent_type: "programmatic"
receive_conversation_history: true
history_limit: 30 # default 20, range 1-200
inputs:
latest_question: "string"
yields:
summary: "string"Where the history lands per agent type:
| Agent type | Field |
|---|---|
programmatic | context["_conversation_history"] |
external (connector) | input_data["_conversation_history"] AND context["_conversation_history"] |
human | included in the webhook payload body |
llm | (no effect — LLM agents already see history via the prompt) |
Human agent backwards compatibility: Human agents that have NOT opted in still use the pre-v2.2.1 hardcoded cap of 5 turns (preserves legacy webhook behaviour). Flip receive_conversation_history: true to switch to history_limit (default 20).
Studio + Playground UI: Both Studio's "Inputs & Outputs" panel and Playground's agent inspector expose a toggle for receive_conversation_history and a number input for history_limit — visible only when agent_type is non-LLM.
Streaming Output
Dual-mode LLM agents (the default LLM type) can stream their output token-by-token to the caller via Server-Sent Events. The caller passes stream=True to mesh_call, the SDK opens an SSE connection, and tokens flow out as they're generated.
agents:
storyteller:
name: "storyteller"
agent_type: "llm"
communication_type: "dual" # required for streaming
model: "gpt-4o-mini"
stream_yield: "narration" # picks which yield field gets streamed
yields:
narration: "string"
sentiment: "string"# Caller-side (Python)
async for chunk in sdk.mesh_call("storyteller", {"prompt": "..."}, stream=True):
if chunk["kind"] == "token":
print(chunk["text"], end="", flush=True)
elif chunk["kind"] == "final":
# full assembled response when the agent finishes
result = chunk["data"]Chunk envelope — three kinds: token (incremental text), final (assembled response dict at the end), error (terminal failure).
Auto-pick rule: If the agent declares exactly ONE yield key, stream_yield can be omitted and the SDK auto-picks that key. If the agent has multiple yields, set stream_yield explicitly.
Falls back cleanly: Non-dual-mode agents (communication_type: chain, execute) and non-LLM agents ignore the stream flag and return the full result as a single final chunk. Your SSE consumer code stays identical.
API surface: POST /api/mesh/request honours stream=true. Studio's Test Modal has a single toggle to enable streaming for debugging.
Accessing Yields in can_call Conditions
can_call.condition expressions read from the agent's parsed yields. Whatever shape your yields produce is what the condition sees.
Flat yields → top-level access
agents:
triage:
name: "triage"
yields:
urgency: number
category: string
can_call:
- agent: specialist
condition: "urgency >= 7"
- agent: billing_handler
condition: "category == 'billing'"Nested object yield → dot-path access
agents:
qualification_agent:
name: "qualification_agent"
yields:
qualification: object # one object yield
can_call:
- agent: assessment_agent
condition: "qualification.match_score >= 30"For this pattern, the condition qualification.match_score >= 30 reads the nested field. Both forms are valid — pick whichever matches how your LLM (or programmatic function) naturally produces output.
Auto-wrap: flat LLM output under a single object yield
LLM prompts often ask for a flat JSON shape (top-level fields), even when the YAML declares yields: { something: object }. A common case:
yields:
qualification: objectprompt: ...
OUTPUT FORMAT: Return only valid JSON.
{
"match_score": 0,
"recommendation": "...",
"summary": "..."
}The LLM does what the prompt asks — returns {match_score: 65, recommendation: "...", ...} at the top level, without a qualification: wrapper.
When all three conditions hold, the SDK reads the customer's intent and treats the whole flat dict as the value of that single yield, so qualification.match_score >= 30 works:
| Schema declares | exactly one yield |
| Type is | object (or dict) |
| LLM returned | a non-empty dict that does not contain the yield key |
Result: qualification = {match_score: 65, recommendation: "...", summary: "..."} — accessible via dot-path in conditions.
This rule does not fire when:
- Multiple yields are declared (ambiguous — which one would the flat dict belong to?)
- The single yield is a scalar (
string,number,boolean,list) - The result already contains the yield key (already wrapped — passes through unchanged)
- The result is empty
{}(treated as "missing", same as before)
If you want explicit nesting end-to-end, change your prompt to wrap:
OUTPUT FORMAT: Return only valid JSON.
{
"qualification": {
"match_score": 0,
"recommendation": "...",
"summary": "..."
}
}Both forms now produce the same qualification yield, so existing configurations don't need to change.
Common gotchas
| Symptom | Cause | Fix |
|---|---|---|
| Condition silently fails, narration routes elsewhere | LLM returned a different shape than declared in yields | Either reshape prompt to match yields, or rely on auto-wrap above for single-object yields |
Failed to evaluate condition: '>=' not supported between instances of 'NoneType' and 'int' in logs | The yield key was missing → defaulted to None/{} → dot-path access against None | Check the YIELDS: ... produced incomplete output warning that fires just before — that names the missing key |
Condition evaluates to False when you expected True | Yield key present but value is the type default ("", 0, [], {}) | Same root cause — your output didn't actually populate the field |
See Message Routing — Condition Evaluation for the full operator reference.
Changing agent_type
You can change an agent's type from Studio's Studio tab (per-agent editor). When agent_type changes, LeafMesh automatically removes fields that don't belong to the new type:
| Switching away from... | Fields removed |
|---|---|
llm | model, prompt, temperature, max_tokens, max_completion_tokens, thinking, reasoning, reasoning_budget, tools, tool_categories, context_parts, tool_choice |
human | webhook_config, human_interface, human_timeout_seconds, channels, is_human_powered |
external | framework, connector_config |
programmatic | integration |
This prevents stale configuration from the old type interfering with the new type's execution.
yields and inputs Type Strings
Values can be a simple type string (flat field) or a nested object definition.
Flat fields — value is a type string:
| Type String | Description |
|---|---|
"string" | Text value |
"number" | Numeric value |
"boolean" | True/false |
"list" | Array/list |
"object" | Dictionary/object (unstructured) |
Nested fields — value is an object with type and fields:
inputs:
# Flat fields
name: string
email: string
# Nested object with defined sub-fields
arguments:
type: object
fields:
summary: string
start_datetime: string
end_datetime: string
timezone: string
yields:
# Flat
status: string
# Nested
data:
type: object
fields:
result: string
metadata: objectWhen a field uses the nested format, the frontend renders individual sub-field editors instead of a single text input. This is useful for external framework integrations (Composio, Zapier, etc.) where the request body has a known structure.
wait_for Expression Syntax
# Simple list — all required (AND)
wait_for:
- agent_a
- agent_b
# Expression strings
wait_for: "agent_a AND agent_b" # Wait for both
wait_for: "agent_a OR agent_b" # Wait for first
wait_for: "agent_a AND agent_b?" # agent_b is optional (? suffix)
wait_for: "agent_a AND (agent_b OR agent_c)" # Nested logic
wait_for: "agent_a AND (agent_b OR agent_c) AND agent_d?" # Complex expressioncan_call Format
# Simple
can_call:
- agent: "next_agent"
# With condition
can_call:
- agent: "next_agent"
- agent: "conditional_agent"
condition: "calling_agent_response.status == 'ready'"Narration Routing
narration is an agent-level field for routing hints you can't express as conditions. Conditions handle definitive routes ("if category is billing, call billing_agent"). Narration handles non-definitive routes ("if the customer sounds frustrated and mentions cancelling, maybe call retention_agent").
agents:
triage:
name: "triage"
yields:
category: "string"
urgency: "number"
can_call:
- agent: "billing_agent"
condition: "category == 'billing'"
- agent: "technical_agent"
condition: "category == 'technical'"
narration: >
If the customer mentions cancelling their subscription, route to retention_agent.
If the customer mentions a competitor by name, route to win_back_agent.
If the customer asks about enterprise plans, route to sales_agent.How it works:
- Conditions are evaluated first by the control plane (AST, instant, deterministic)
- Condition targets are dispatched immediately
- The Manager's analysis pass reads the narration alongside the agent output
- The Manager recommends additional next agents based on both condition results and narration hints
- The Manager calls any narration-suggested agents that conditions didn't already dispatch
Key rules:
- Conditions are the authority — narration never overrides a condition result
- Narration targets are additive — they add to condition targets, never remove
- Narration can reference any agent in the mesh, not just those in
can_call - No narration = zero overhead
- If the Manager is disabled, narrations are ignored
See Manager — Narration Routing and Message Routing for the full flow.
Knowledge Config
knowledge enables RAG-powered context injection from a vector database. The agent gets both pre-call injection (automatic) and a query_knowledge tool (on-demand) — same dual pattern as memory.
agents:
support_agent:
name: "support_agent"
knowledge:
serviceName: "mongo_main"
enabled: true
groupName: "product_docs"| Field | Type | Required | Default | Description |
|---|---|---|---|---|
serviceName | string | yes | — | The knowledge service's name (its store and embeddings are set up once for the mesh, not per agent) |
enabled | bool | no | true | false keeps the setting and withholds the knowledge tool — the agent stops querying it |
groupName | string | no | null | Query a specific group. Omit to query all groups. |
Key points:
- Knowledge store and embedding credentials and addresses (connection strings, API keys, hosts) are entered only in the vault, under Infrastructure → Knowledge (MongoDB, pgvector, Pinecone), never in YAML
- When enabled, both retrieval paths are active automatically — no mode flag
query_knowledgetool is stripped from agents without knowledge enabled- The Manager can also have knowledge for SOP awareness (configured under
manager.knowledge)
See Manager — Narration Routing for how knowledge integrates with Manager analysis.
LLM Agent Fields (agent_type: "llm")
These fields are used when agent_type is "llm". Ignored for other types.
| Field | Type | Default | Accepted Values | Description |
|---|---|---|---|---|
model | string | none — required | see [Model List](#model-list) below | LLM model name. The SDK never picks one for you: since 2.4.135 there is no default, boot names every LLM agent missing one, and a call raises ModelNotConfiguredError. Non-LLM agents need none. |
model_connection | string | null | a vault record name | 2.4.486+. The vault record this agent's model calls sign in with. Unset: the provider's own record. A missing record, one with no key or one for another provider fails that call with a plain message naming the record; it never falls back to another key. For Bedrock, Vertex and Foundry it is resolved as that cloud's sign-in. |
prompt | string | null | any string (multiline supported) | System prompt |
temperature | float | 0.1 | 0.0 – 2.0 | LLM temperature (creativity/randomness) |
max_tokens | int | 800 | 1 – model max | Max output tokens (legacy models) |
max_completion_tokens | int | null | 1 – model max | Max completion tokens (o-series, GPT-5.x and GPT-6 models) |
thinking | bool | false | true, false | v2.2.24+: SDK reasoning tools — chain_of_thought, metacognitive_reflection and apply_edits — on any model. Text only: thinking never runs code; use sandbox. Was reasoning pre-v2.2.24. |
sandbox | bool or dict | off | true, or {network: {allow: [...]}, timeout_seconds, cpu_seconds, memory_mb, workspace_mb} | Run code in a contained sandbox through the sandbox_exec(code, language) tool (python or shell). true gives the safe defaults: no network, timeout_seconds 30 (1–600), cpu_seconds 5 (1–300), memory_mb 256 (1–4096), workspace_mb 64 (1–1024). A map changes only the fields it names; network.allow lists host:port entries, the only hosts the code may reach. On a host that cannot contain code the tool is withheld and the config still loads. See [LLM Agents → Running Code](../agents/llm-agents#running-code-sandbox). |
framework_guidance | bool | true | true, false | Add the SDK's short guide to the agent's runtime tools (memory, knowledge, reasoning, the shared board) to its system prompt. Set false when the agent's own prompt already covers them. Where the agent is from and the security guardrail are always added. |
image_options | dict | null | provider-specific keys | Options for an image-generation agent (output_modality: image): size and quality (OpenAI); n and aspect_ratio (Vertex Imagen); n, width, height and aspect_ratio (Bedrock). Keys a provider does not use are ignored. |
reasoning | bool | false | true, false | v2.2.24+: Native model reasoning via provider API. Requires a reasoning-capable model (o-series, GPT-5.x / GPT-6, Claude 3.7+, Gemini 2.5+, Grok 4.5+). Was thinking pre-v2.2.24. |
reasoning_budget | int | null | 1024 – 32768 (tokens) | v2.2.24+: Max native reasoning tokens. Was thinking_budget pre-v2.2.24 (which is still accepted as a deprecated alias). |
enable_prompt_caching | bool | false | true, false | Enable provider-native prompt caching for cost reduction (see below) |
cache | bool | null | true, false | LLM-text cache opt-out (2.4.107). The SDK cache reuses ONLY the model's text; your function and decorators always run fresh. false disables even that reuse. From 2.4.487 only answers that are safe to reuse are stored: no personal data or secret, and only when every tool the turn used changes nothing (see [LLM Response Caching](../models/caching)) |
effort | string | null | low, medium, high | Reasoning-effort hint for models that support it — depth vs. latency/cost trade |
skills | list | [] | skill names | Skill packs loaded into this agent's prompt context (LLM agents only) |
optimization_strategy | string | null | performance, cost, speed | Per-agent adaptive model selection. When set, overrides model with the predictor's pick; the configured model becomes the fallback. See [LLM Agents → Adaptive Model Selection](../agents/llm-agents#adaptive-model-selection). |
context_parts | dict | null | see below | Optional context parts |
tools | list | [] | tool name strings | Available tools |
tool_choice | string | "auto" | auto, none, or specific tool name | Tool selection strategy |
max_tool_calls_per_message | int | 5 | 0 – 20 | Max tool calls per LLM message |
tool_call_timeout | float | 30.0 | 0.1 – 300 (seconds) | Tool execution timeout |
llm_hard_timeout_s | float | null | > 0 (seconds) | Per-agent LLM hard-timeout ceiling. Falls back to the `timeouts.llm_hard_timeout_s` config field, then a 90s default. If a call exceeds the ceiling it is retried once at the same ceiling before failing. |
allow_parallel_tool_calls | bool | true | true, false | Allow parallel tool execution within a turn (bounded by the `limits.max_parallel_tool_calls config field, default 8). Set false` to run a turn's tools strictly one after another. |
tool_categories | list | [] | category name strings | Tool categories agent can access |
strict_tools | bool | false | true, false | Opt into provider strict tool schemas (the model cannot pass hallucinated/extra arguments). |
super_agent | bool or dict | false | true, false, or a tuning map | Wrap the agent in the planning orchestrator (todo-based plan → per-step execution → synthesis). In a map, enabled: false keeps the tuning and turns planning off. From 2.4.491 tries: {mode, count, rounds} sets how hard each step works to pass its check (default one attempt plus one retry). See [Super Agent](../agents/super-agent). |
memory_strategy | string | null | recency, relevant, hybrid | How prior activity is selected for recall when memory is enabled. |
knowledge_strategy | string | null | inject, tool, both | Whether configured knowledge is pre-injected into the prompt, offered as the query_knowledge tool, or both. |
monthly_token_budget | int | null | > 0 | Per-agent monthly token budget. When exceeded, the agent stops per budget_on_exhaust. Opt-in — unset means no cap. |
monthly_cost_budget | float | null | > 0 (USD) | Per-agent monthly cost budget. When exceeded, the agent stops per budget_on_exhaust. Opt-in. |
budget_on_exhaust | string | "graceful" | graceful, escalate | On budget exhaustion: graceful fills declared yields with a budget-exhausted marker and ends cleanly; escalate routes to the Manager error path. |
schedule_targets | list | null | agent name strings | Agents this agent may target with [schedule_followup](../tools/schedule-followup). Defaults to the agent's can_call set. |
max_followup_depth | int | 5 | > 0 | Max depth of self-scheduled follow-up chains (continuations of continuations) before the tool refuses. |
max_followups_per_session | int | 20 | > 0 | Max follow-ups an agent may schedule per session. |
LLM agents only. Tools and the tool blocks (zapier, composio, n8n, mcp), the playbook, follow-ups and their limits (schedule_followup, schedule_targets, max_followup_depth, max_followups_per_session), image output (output_modality, image_options), eager skills and the sandbox are refused at load on human, external and programmatic agents, and dropped when an agent's type changes.
Removed from agents: custom_endpoint and custom_headers. The model server and its headers are the mesh's setting — set them once in mesh.local. An agent that still has them loads with a warning, and they have no effect. The same goes for a per-agent kanban: block: the board is the mesh's (see Shared Board).
context_parts Keys
Each key is injected as a separate system message with a bracketed label, in the order below. Custom keys are also supported — they receive an auto-generated label from their name (MY_KEY → [MY KEY]).
| Key | Label injected | Description |
|---|---|---|
care | [EMPATHY & TONE] | Warmth/empathy instructions — shapes how the agent expresses itself |
sentiment_analysis | [SENTIMENT ANALYSIS] | Tone detection instructions — tells the agent to read user mood |
guardrails | [SAFETY GUARDRAILS] | Safety and compliance rules — what the agent must never do |
flows | [FLOW INSTRUCTIONS] | Per-caller routing behaviour — what the agent should do differently depending on who called it and where in the mesh it is |
Values are free text strings. All keys are optional — use any combination.
context_parts:
care: |
Always respond with empathy. Acknowledge frustration before solving.
guardrails: |
Never share internal system details. No PII disclosure.
flows: |
When called from the entry point (no from_agent):
- This is a new user. Greet warmly and gather requirements.
When called from client (human agent):
- The human has already responded. Don't re-greet. Summarise and proceed.
When called from scheduler_agent:
- This is a scheduled run. Skip greeting, produce a structured summary.Model List
These are the models Studio's model picker offers as of SDK 2.4.436, plus the ones marked 2.4.508. Every key below can also be saved on the Infrastructure page instead of set as an environment variable.
OpenAI: (OPENAI_API_KEY)
gpt-6-astra,gpt-6.1-sol,gpt-6.1-terra(2.4.508),gpt-6-sol,gpt-6-lunagpt-5.6-sol,gpt-5.6-terra,gpt-5.6-lunagpt-5.5,gpt-5.4,gpt-5.4-mini,gpt-5.4-nano,gpt-5.2,gpt-5.1,gpt-5,gpt-5-mini,gpt-5-nanogpt-4.1,gpt-4.1-mini,gpt-4.1-nano,gpt-4o,gpt-4o-minio3,o4-mini- GPT-6, GPT-5.6 and the
-promodels call tools through OpenAI's Responses API; LeafMesh switches to it automatically.
Anthropic: (ANTHROPIC_API_KEY)
claude-fable-5-1,claude-fable-5claude-opus-5-5,claude-opus-5,claude-sonnet-5-5,claude-sonnet-5,claude-haiku-5-5claude-opus-4-8,claude-opus-4-7,claude-opus-4-6,claude-sonnet-4-6,claude-sonnet-4-5,claude-haiku-4-5
Google: (GOOGLE_API_KEY / GEMINI_API_KEY)
gemini-4-argon,gemini-omni-1.2-flash(2.4.508)gemini-3.8-flash,gemini-3.6-flash,gemini-3.5-flash-litegemini-3.1-pro-preview,gemini-3.1-flash-lite,gemini-3-flash-previewgemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite
DeepSeek: (DEEPSEEK_API_KEY)
deepseek-v4-pro,deepseek-flash- Priced by time of day: calls in DeepSeek's busy hours cost twice the off-peak price.
deepseek-chatanddeepseek-reasonerare retired.
Mistral: (MISTRAL_API_KEY) (v2.2.25)
mistral-large-4-0,mistral-medium-3-5-26-04,mistral-small-4-0-26-03,mistral-large-3-25-12codestral-2508- Self-hosted: set
MISTRAL_BASE_URLto override the defaulthttps://api.mistral.ai/v1endpoint
xAI Grok: (XAI_API_KEY / GROK_API_KEY) (v2.2.25)
grok-4.7,grok-4.6,grok-4.5,grok-4.3grok-4.20-0309-reasoning,grok-4.20-0309-non-reasoning,grok-build-0.1
Other model vendors (one key each; see LLM Providers → Other Model Vendors):
- By model name:
kimi-k3,kimi-k3.1-pro,kimi-k3.1-pro-highspeed(Moonshot,MOONSHOT_API_KEY),qwen3.8-max/qwen3.7-plus/qwen3.7-flash(Alibaba,DASHSCOPE_API_KEY),glm-5.3/glm-5.3-flash(Z.AI,ZAI_API_KEY),minimax-m3/minimax-m3.1-flash(MINIMAX_API_KEY),mercury-3(Inception,INCEPTION_API_KEY),muse-spark-1.3/muse-wonder-1.0(Meta,META_API_KEY),step-5-preview/step-3.7-flash(StepFun,STEPFUN_API_KEY),trinity-large-thinking(Arcee,ARCEE_API_KEY) - Also by model name, using ids from the vendor's own list: Tencent Hunyuan (
hy3…,hy4…,hunyuan-…,HUNYUAN_API_KEY), Xiaomi MiMo (mimo-…,MIMO_API_KEY), Upstage (solar-…,UPSTAGE_API_KEY), Reka (reka-…,REKA_API_KEY), Cohere (command-…,CO_API_KEY), Poolside (poolside/…, sent as written,POOLSIDE_API_KEY) - By host prefix (removed before sending):
openrouter/<lab>/<model>(OPENROUTER_API_KEY),groq/<model>(GROQ_API_KEY),cerebras/<model>(CEREBRAS_API_KEY)
Image models (for an agent with output_modality: image):
- OpenAI:
gpt-image-2.6-flare,gpt-image-2.6-sunburst(2.4.508),gpt-image-2,gpt-image-1.5,gpt-image-1-mini(2.4.508),gpt-image-1,dall-e-3,dall-e-2 - Google:
gemini-nano-banana-2.1(2.4.508),gemini-3-pro-image,gemini-3.1-flash-image,gemini-2.5-flash-image - Billed by kind of token (text, thinking, image in, image out) from the vendor's own counts (2.4.508)
AWS Bedrock:
- Any Bedrock model ID (e.g.
anthropic.claude-sonnet-4-20250514-v1:0,amazon.titan-text-premier-v1:0) - Requires
mesh.bedrockconfig
Google Vertex AI:
- Any Vertex model ID (e.g.
gemini-2.5-pro) - Requires
mesh.vertexconfig
Azure Foundry:
- Any Azure deployment name
- Requires
mesh.foundryconfig
Local Models (vLLM, SGLang, Ollama, llama.cpp, etc.):
- Any model name supported by your local server
- Requires
mesh.localconfig (orLOCAL_MODEL_ENDPOINTenv var) - See LocalModelConfig for server setup
thinking vs reasoning (v2.2.24+ semantics)
These are two separate features. The YAML key meanings were swapped in v2.2.24 to align with provider terminology — see LLM Agents → v2.2.24 migration.
| Feature | thinking: true | reasoning: true |
|---|---|---|
| What it does | SDK adds three text tools (chain_of_thought, metacognitive_reflection, apply_edits) to the available tool list. It never runs code — that is sandbox | Enables provider-native extended reasoning / thinking |
| Works with | Any model (tool injection) | Only models that support native reasoning |
| Token cost | Tool-call overhead | Dedicated reasoning tokens (billed as output) |
| Quality | Good for structured CoT scaffolding | Best quality — model's internal reasoning |
You can use both together — thinking adds SDK reflection tools while reasoning enables the model's own native chain-of-thought.
Native Reasoning — Provider Support
| Provider | Model Requirement | Behavior |
|---|---|---|
| Anthropic | Claude 3.7+ | Sends thinking: {type:enabled, budget_tokens:N} on models that take a token budget (Sonnet 4.6, Haiku 4.5 and older). Adaptive-only models (Opus 4.7 / 4.8, Opus 5.x, Sonnet 5.x, Haiku 5.5, Fable, Mythos) auto-skip the manual parameter — the model decides for itself, with an effort level. |
| OpenAI | o-series (o3/o4-mini), GPT-5.x, GPT-6 | A reasoning effort level. GPT-6, GPT-5.6 and the -pro models go through OpenAI's Responses API automatically; the effort none is never sent to GPT-6 Astra or GPT-6.1 Sol, which reject it. |
Gemini 2.5+ (uses thinking_budget), Gemini 3.x (uses thinking_level: low/medium/high) | Family-aware param selection — provider auto-picks the right field per model. | |
| DeepSeek | The reasoner family only (deepseek-reasoner, now retired by DeepSeek) | Implicit via model name. The current models (deepseek-v4-pro, deepseek-flash) are not treated as the reasoner family, so reasoning: true adds nothing there — use thinking: true for SDK chain-of-thought. |
| Bedrock | Claude models on Bedrock (same Anthropic rules) | Writes additionalModelRequestFields.thinking (NOT inferenceConfig.thinking — that location is rejected). |
| Vertex | Claude + Gemini on Vertex | Both paths supported with the same family rules. |
| Foundry | Azure o-series + gpt-5.x deployments | reasoning_effort: str (same kwarg shape as direct OpenAI). |
| Mistral *(v2.2.25)* | No native reasoning API as of v2.2.25 | reasoning: true is silently ignored with an informational log. Use thinking: true for SDK chain-of-thought scaffolding. |
| xAI Grok *(v2.2.25)* | grok-4.5, grok-4.6, grok-4.7 (and grok-4-mini) | A reasoning effort: low / medium / high on 4.5, plus xhigh on 4.6 and 4.7; low / high on other Grok models. The SDK sends the highest level the model accepts that is not above the one asked for. |
| Local | Depends on model/server | Passthrough if server supports it. |
Prompt Caching — Provider Support
| Provider | How it works | Savings |
|---|---|---|
| Anthropic | cache_control: ephemeral on system prompt + tools | ~90% on cached reads |
| Bedrock | promptCaching parameter | ~90% on cached reads |
| Vertex (Claude) | cache_control: ephemeral on system prompt | ~90% on cached reads |
| OpenAI | Automatic — no config needed (stats in response) | ~50% on cached |
| Context caching API (requires separate setup) | Varies |
Structured Output — Derived from yields
You do not declare a separate response_format field. The agent's yields schema is the single source of truth — at runtime the SDK auto-builds a provider-appropriate JSON enforcement shape from it.
agents:
data_extractor:
agent_type: llm
model: gpt-4o
prompt: "Extract structured data from the user's message."
yields: # ← this IS the output contract
name: string
email: string
priority: string| Provider | What the SDK applies under the hood |
|---|---|
| OpenAI | response_format = {"type": "json_object"} (basic JSON mode) |
| DeepSeek | response_format = {"type": "json_object"} |
| Foundry | response_format = {"type": "json_object"} |
| Local | response_format = {"type": "json_object"} |
| Anthropic | Tool-based JSON extraction using the derived schema |
| Bedrock | Schema injected into system prompt |
| Google / Vertex | response_schema in generation config |
If the LLM produces non-JSON or output that doesn't match the yields keys, the SDK runs one reflection retry; if that also fails, the agent returns a clean error instead of silently substituting defaults.
Example
agents:
analyst:
name: "analyst"
agent_type: llm
model: claude-sonnet-4-6
reasoning: true # native model reasoning (provider extended thinking)
reasoning_budget: 8192 # max 8K native-reasoning tokens
thinking: true # also add the SDK's text-only reasoning tools
enable_prompt_caching: true # cache system prompt + tools
prompt: |
You are a data analyst. Analyze the provided data thoroughly.Human Agent Fields (agent_type: "human")
These fields are used when agent_type is "human". is_human_powered is auto-set to true.
| Field | Type | Default | Accepted Values | Description |
|---|---|---|---|---|
is_human_powered | bool | false | true, false | Auto-synced to true when agent_type="human" |
human_interface | string | "api" | default, api, webhook, custom | How human receives/submits input (see below) |
human_timeout_seconds | int | 300 | 1 – 3600 (seconds) | Human response timeout |
human_context_template | string | null | any string | Template for presenting context to human |
human_prompt_template | string | null | any string | Template for human prompts |
fallback_on_timeout | bool | false | true, false | v2.4.82: default false — a human timeout escalates/errors and is never treated as an assumed answer. Set true to opt into using fallback_response and continuing the chain. |
fallback_response | dict | null | arbitrary JSON | Default response on human timeout |
operator_ids | list | [] | list of strings (email or ID) | Operators who can see this agent's HITL requests. Empty = broadcast (all operators see it). |
webhook_config | object | null | see [WebhookConfig](#webhookconfig) | Webhook settings (required for webhook interface) |
channels | dict | {} | see [ChannelConfig](#channelconfig) | Native channel adapters (Slack, Telegram, etc.) |
Removed in 2.4.382: approval is routing
require_human_confirmation, confirmation_seat and human_escalation_triggers no longer exist. A config that sets one of them on (true, a seat name, or a non-empty trigger list) is refused at load, because its author expected a person to see the work first. One that is off or empty is dropped, so old Studio defaults still load. To have a person approve work, route it to a human agent with can_call and a condition, and let that person's agent call the next one:
- name: refund_agent
can_call:
- agent: finance_lead
condition: "needs_approval == true"
- name: finance_lead
agent_type: human
can_call:
- agent: payout_agenthuman_interface — Interface Types
| Value | Description | How it works |
|---|---|---|
default | Built-in HITL Inbox (recommended) | Persists the request and emits a live event. Studio Frontend renders an inbox with conversation thread. Human replies via the UI. Supports parallel requests per session. |
webhook | External webhook | POSTs request to the address on the webhook_config record (an Outbound webhook record). Human responds via inbound webhook endpoint. Also supports native channel adapters (Slack, Telegram, etc.). |
api | Python callback | Calls a Python handler set on the human agent instance via human_agent.set_human_interface_handler(handler) after sdk.start(). Retrieve the agent from sdk.agent_registry.get_agent("<name>"). No outbound HTTP. Used for custom integrations and testing. |
custom | Custom handler | Same as api — uses the registered human_interface_handler callback. |
Note:
defaultmode writes the request to Redis and emits a stream event for an inbox UI to pick up — Studio renders this out of the box. If you're not running Studio, pointwebhookat your own service through an Outbound webhook record or useapiwith a Python callback instead.
Example — Default (Studio Frontend Inbox)
agents:
support_human:
name: "support_human"
agent_type: human
human_interface: default # Studio Frontend inbox
human_timeout_seconds: 300
yields:
resolution: string
action_taken: stringExample — Webhook with Channel Adapter
agents:
support_human:
name: "support_human"
agent_type: human
human_interface: webhook
human_timeout_seconds: 600
webhook_config:
connection: "human-requests" # Outbound webhook record: the address of your app
channels:
slack:
connection: "slack-workspace" # Infrastructure → Messaging
post_channel: "C123456"Two Scenarios for Human Agent Sessions
Human agents support two interaction patterns via the same webhook endpoint:
Scenario 1 — Resume (session_id present): When a POST to /webhook/{entry_point} includes a session_id that matches a pending HITL request, LeafMesh resumes that session. The operator's response is routed via can_call to the next agent.
Scenario 2 — New (no session_id or not found): When a POST has no session_id or the session has no pending request, LeafMesh creates a new workflow. If the human agent has no upstream caller, the operator's message is immediately routed via can_call — no HITL pending step is created.
External Agent Fields (agent_type: "external")
These fields are used when agent_type is "external".
| Field | Type | Default | Accepted Values | Required | Description |
|---|---|---|---|---|---|
framework | string | null | crewai, langgraph, autogen, a2a, servicenow, sap_joule, workday, box, atlassian_rovo, gemini_enterprise, mcp, zapier, composio, n8n, hermes, lyzr, writer, dify, flowise, vellum, stackai, agentforce, watsonx, relevance, wordware, claude_agent, claude_managed, openai_agents, openai_agents_api, foundry_agents, vertex_agent_engine, bedrock_agentcore, copilot_studio, mistral_agents, letta, glean_agents, lindy, custom_api, elevenlabs_agents, vapi, retell, bland, custom | yes | External framework name. The four phone-call platforms (elevenlabs_agents, vapi, retell, bland) are new in 2.4.508. See [External Frameworks → Supported Frameworks](../integrations/external-frameworks#supported-frameworks). voiceflow and cognigy were removed and are refused at load |
connector_config | dict | {} | framework-specific key-values | no | Connection configuration — passed as **kwargs to the connector. Addresses and credentials come from the record named by connection: (see below); an mcp connector's env may use ${vault:<field>} |
request_form | dict | {} | up to 50 keys (1–64 of A-Z a-z 0-9 _ - ., not starting with _), 16 KB of JSON in all; no secrets | no | Your own fields, sent with every request in the framework's own place for caller data (n8n webhook body, A2A message metadata, LangGraph config.configurable, MCP tool arguments, …). Refused at load on writer, watsonx, autogen, hermes, relevance, claude_agent, openai_agents, openai_agents_api, letta and custom. Also valid on a programmatic agent with an integration. See [External Frameworks → request_form](../integrations/external-frameworks) |
| price_per_1k_tokens | float | null | ≥ 0 | no | For a platform that does not report what a call costs: the price per 1,000 tokens, so the mesh's estimate of each call has a cost and a monthly_cost_budget applies. Unset: only a token budget applies to an estimated agent. Also valid on a programmatic agent with an integration. See Per-Agent Budgets |
One agent = one action/workflow. Each agent targets one specific endpoint, graph, tool, or action. Create multiple agents with different
connector_configvalues to call different workflows.All
connector_configfields can also be overridden per-call viarequest.connector_configat runtime.
Common connector_config fields (all frameworks)
connection (a vault connection name) is available on every connector type. after_handoff: wait and move_on are offered by n8n, Zapier and Lindy (and the phone-call platforms, which take only those two); every other framework takes reply only:
| Field | Type | Default | Description |
|---|---|---|---|
connection | string | — | Vault connection that supplies this connector's credential and account fields (on Zapier, use vault_connection — its connection is the app) |
after_handoff | string | "reply" (n8n, Zapier); "wait" (Lindy, phone-call platforms) | 2.4.508 (was `mode`). What happens after the hand-off: reply, wait or move_on — see below. A configuration that still says mode is refused at load |
callback_timeout | float | 120.0 (n8n, Zapier, Lindy); 1800 (phone-call platforms) | Seconds wait or move_on allows for the answer to come back. Past it the agent fails with "no result came back from <platform> within Ns" and the failure review takes over. Read on each call, so a change applies to the next one |
After the hand-off: reply, wait, move_on (2.4.508)
2.4.508 renamed mode: mode: sync → after_handoff: reply, mode: callback → after_handoff: wait. A configuration that still says mode is refused at load; a Python helper given mode= raises ValueError carrying the new line.
reply (default on n8n and Zapier): the connector POSTs to the external system and the HTTP response is the answer. Use it when the system returns the result in the same response.
wait and move_on: the connector POSTs with _leafmesh_callback_url and _leafmesh_session_id in the payload, and the agent's run ends at once — no worker is held. Its output is {"status": "waiting_for_callback" | "handed_off", "after_handoff", "agent", "framework", "message", "call_id"} (call_id only for phone-call platforms), and nothing routes on it. The hand-off is kept in the store and survives a restart. When the system POSTs the result to /callback/{agent_name}, the agent runs again with that answer as its output, and its can_call routes run on the answer. The difference is the board: wait keeps the step "asked, waiting"; move_on records the hand-off as done. Use them when:
- The external workflow takes longer than the HTTP timeout
- The external system answers later (e.g., n8n's "Respond Immediately" mode)
- You need the external system to process asynchronously and deliver results later
The webhook is signed, so it needs LEAFMESH_WEBHOOK_SECRET in .env (2.4.507: the mesh no longer derives one; webhooks answer 503 until it is set).
Python helpers always wait in place, because the code needs the answer. n8n() and zapier() take after_handoff="reply" (default) or "wait"; mcp() and composio() take "reply" only, and the mesh refuses "wait" for them at load. "move_on" adds nothing there. callback_timeout= keeps its name.
How external systems use callbacks:
The LeafMesh connector injects these fields into the outbound payload:
_leafmesh_callback_url— the URL to POST the result back to_leafmesh_session_id— the session ID to include in the callback
The external system should POST to _leafmesh_callback_url with a JSON body containing:
{
"session_id": "<the _leafmesh_session_id value>",
"result": { ... }
}connector_config fields per framework
From 2.4.481 every field typed record below (addresses — endpoint, url,
webhook_url, base_url, mcp_endpoint — and credentials — api_key,
user_api_key, auth_token, mcp_key, headers) is entered only in the vault.
Make the record under Infrastructure → External agents → the framework
(Zapier, n8n, Composio and MCP are under Infrastructure → Integrations),
and name it with connection: (Zapier: vault_connection:). A record field
written inline in connector_config, including an ${ENV} reference, is
refused at load, and the error names the field and the record to create.
crewai
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
endpoint | record | "" | yes | HTTP endpoint for deployed CrewAI crew |
api_key | record | "" | no | Bearer Token for authentication |
user_api_key | record | "" | no | User Bearer Token (preferred over api_key when both are set) |
poll_interval | float | 2.0 | no | Seconds between status polls |
max_poll_seconds | float | 300.0 | no | Max total polling time (seconds) |
http_timeout | float | 30.0 | no | HTTP request timeout (seconds) |
langgraph
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
endpoint | record | "" | yes | LangGraph Platform deployment URL |
api_key | record | "" | no | API key |
graph_id | string | "agent" | no | Which graph to run — this is the workflow selector |
poll_interval | float | 1.0 | no | Seconds between status polls |
max_poll_seconds | float | 300.0 | no | Max total polling time (seconds) |
http_timeout | float | 30.0 | no | HTTP request timeout (seconds) |
autogen
Connects to an external AutoGen Studio or custom AutoGen API service via HTTP.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
endpoint | record | "" | yes | AutoGen service base URL (e.g. http://localhost:8081) |
api_key | record | "" | no | Bearer token for authentication |
workflow_id | string | "" | no | Workflow/agent ID to execute on the AutoGen service |
timeout | float | 120.0 | no | HTTP request timeout (seconds) |
poll_interval | float | 2.0 | no | Seconds between status poll requests |
max_poll_seconds | float | 300.0 | no | Max total polling time (seconds) |
a2a
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
url | record | "" | yes | A2A-compatible agent server base URL |
auth_token | record | "" | no | Bearer token for authentication |
auth_scheme | string | "Bearer" | no | Authorization header scheme |
poll_interval | float | 2.0 | no | Seconds between task status polls |
max_poll_seconds | float | 300.0 | no | Max total polling time (seconds) |
http_timeout | float | 30.0 | no | HTTP request timeout (seconds) |
mcp
MCP supports two transport modes. tool_name is always required.
Common fields (both transports):
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
tool_name | string | "" | yes | Which MCP tool to call — the workflow selector |
transport | string | "stdio" | no | Transport mode: "stdio" or "http" |
timeout | float | 60.0 | no | Request timeout (seconds) |
stdio transport fields:
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
command | string | "" | yes (stdio) | Executable to launch (e.g. "npx") |
args | list | [] | no | Command arguments (e.g. ["-y", "@mcp/server-npm"]) |
env | dict | null | no | Environment variables for the subprocess |
http transport fields:
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
url | record | "" | yes (http) | MCP server HTTP/SSE endpoint |
auth_token | record | "" | no | Bearer token |
zapier
Tool name is built as {connection}_{action} (e.g. google_sheets_create_row).
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
connection | string | "" | yes* | Zapier app name (e.g. "google_sheets", "slack", "gmail") |
action | string | "" | yes* | Action name (e.g. "create_row", "send_message") |
mcp_key | record | "" | yes | Zapier MCP key, on the record |
vault_connection | string | — | yes | The Zapier record holding mcp_key (and any mcp_endpoint override) |
mcp_endpoint | record | Zapier's MCP server | no | Override the Zapier MCP address |
timeout | float | 60.0 | no | HTTP request timeout (seconds) |
*At least one of connection or action required for the tool name.
Zapier retired AI Actions, the REST API that api_key reached. A Zapier agent with only an api_key fails with that explanation; prefer_mcp and instructions are no longer read.
composio
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
action | string | "" | yes | Composio action enum (e.g. "GITHUB_STAR_A_REPOSITORY") — the workflow selector |
entity_id | string | "default" | no | User/entity context for managed auth |
api_key | record | "" | no | Composio API key |
timeout | float | 60.0 | no | Execution timeout (seconds) |
n8n
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
webhook_url | record | "" | yes | Full webhook trigger URL — one URL per n8n workflow |
auth_token | record | "" | no | Bearer token |
api_key | record | "" | no | The instance's n8n API key (execution list + read). Optional: with it, each node of the call's own execution is reported as a step |
base_url | record | the webhook's host | no | The instance address, when the n8n API is not on the webhook's host |
timeout | float | 60.0 | no | HTTP request timeout (seconds, after_handoff: reply only) |
n8n webhook URL types:
- Production:
https://your-instance.app.n8n.cloud/webhook/<id>— works when workflow is activated (toggle ON) - Test:
https://your-instance.app.n8n.cloud/webhook-test/<id>— only works while n8n editor has "Listen for Test Event" active (one-shot, for development only)
n8n + after_handoff: wait:
With after_handoff: "wait" (or "move_on"), the n8n workflow should:
- Start with a Webhook trigger node (receives the payload including
_leafmesh_callback_url) - Configure the Webhook node to "Respond Immediately" (optional —
replyworks too) - Process the workflow
- End with an HTTP Request node that POSTs back to
{{ $json._leafmesh_callback_url }}with body:{ "session_id": "{{ $json._leafmesh_session_id }}", "result": { ... } }
Phone-call platforms (2.4.508)
elevenlabs_agents, vapi, retell and bland: the platform makes the phone call and the outcome comes back by webhook. The agent's input carries phone_number (E.164, e.g. +14155550100); its other text, number and yes/no fields become the call's variables. Set the platform's webhook address to <mesh>/callback/calls/<framework> (signed, so it needs LEAFMESH_WEBHOOK_SECRET). The account's key, webhook signing secret and account ids come from connection.
| Field | Default | Frameworks | Description |
|---|---|---|---|
after_handoff | "wait" | all four | wait or move_on; reply is refused (a platform never answers in the reply) |
callback_timeout | 1800 | all four | Seconds allowed for the call's outcome |
http_timeout | 30 | all four | Timeout for the request that starts the call (seconds) |
agent_id | — | elevenlabs_agents (required), retell, bland | Kept on the connection — the platform's agent |
phone_number_id | — | elevenlabs_agents, vapi (required) | Kept on the connection — the platform's id for the number the call is made from |
assistant_id | — | vapi (required) | The Vapi assistant |
from_number | — | retell (required), bland | The number the call is made from (E.164) |
pathway_id, persona_id, task | — | bland | What runs the call: one of these or agent_id is required |
line | "twilio" | elevenlabs_agents | twilio or sip_trunk |
See External Frameworks → Phone-call platforms.
Programmatic Agent Fields (agent_type: "programmatic")
These fields are used when agent_type is "programmatic".
| Field | Type | Default | Accepted Values | Description |
|---|---|---|---|---|
integration | string | null | zapier, composio, n8n, mcp, python | Integration connector (optional). `python` (2.4.492+), Pure Python: the agent runs your own function, marked @sdk("<name>") or @sdk.intelligence("<name>") (the same thing). If that function is not in the running process, the agent is switched off with: *needs its Python function — add @sdk("<name>") (or @sdk.intelligence) in the project's code and redeploy; it was not found in this process*. An agent with a connector, or with no integration, is never switched off for missing code |
connector_config | dict | {} | integration-specific key-values | Same fields as the matching external framework connector — see tables above |
Validation rule: integration is only valid when agent_type is "programmatic".
When integration is set, connector_config is passed as **kwargs to the connector's __init__. Use the same fields as the matching framework in the tables above (zapier → zapier fields, mcp → mcp fields, etc.).
The common fields (after_handoff, callback_timeout) are also available here (2.4.508; mode is refused at load). Use reply or wait.
WebhookConfig
Used inside webhook_config for human agents. A seat that needs no webhook is
an Inbox seat: human_interface: default (no webhook or channel needed).
The webhook's address (base URL), header and query rows, and the
inbound_token secret are an Add custom record on Studio's Infrastructure
page with service type Outbound webhook. webhook_config names it with
connection:. From 2.4.481 outbound_url, outbound_headers and
inbound_auth_token written inline are refused at load.
| Field | Type | Default | Description |
|---|---|---|---|
connection | string | null | The Outbound webhook record: address, header and query rows, inbound token |
outbound_timeout | int | 30 | Timeout for outbound calls (seconds) |
inbound_endpoint | string | null | Endpoint path for inbound responses (e.g. "/webhook/human_contact"). If not set, derived from entry points. |
response_mapping | dict | {} | Field mapping for webhook response transformation |
max_retries | int | 3 | Max retry attempts for failed outbound webhooks |
retry_delay | int | 5 | Delay between retries (seconds) |
operators | dict | {} | Operator ID → recipient for this webhook |
The full shape: webhook_config: {connection: <name>, outbound_timeout: 30, inbound_endpoint: /path, response_mapping: {}, max_retries: 3, retry_delay: 5, operators: {}}. There is no method field.
ChannelConfig
Used inside channels dict for human agents. Keys are provider names.
Supported Provider Keys
| Key | Provider |
|---|---|
slack | Slack Bot API |
telegram | Telegram Bot API |
discord | Discord Bot API (coming) |
whatsapp | WhatsApp Business API (Meta Cloud API) (coming) |
teams | Microsoft Teams Bot Framework |
email | SMTP outbound + Mailgun / SendGrid / Postmark inbound — see [Email Channel](#email-channel) |
Fields Per Channel
Each channel's credentials are a record under Infrastructure → Messaging
(Slack, Microsoft Teams, Telegram, Zoho Cliq; Discord and WhatsApp coming); email under
Infrastructure → Workspace → Email. The channel names it with
connection:. Tokens and secrets written inline, including ${ENV}
references, are refused at load (the connection name itself may come from
the environment: connection: "${SLACK_CONNECTION:slack-workspace}" is fine).
On the record, never in YAML: tokens and secrets, addresses (url,
base_url, api_base, endpoint, service_url, webhook_url,
execution_url, accounts_url), storage_url, smtp_host / smtp_port /
smtp_username, client_id, app_id, tenant_id and aws_access_key_id.
inbound_domain, from_address and post_channel stay in YAML.
| Field | Type | Default | Description |
|---|---|---|---|
connection | string | null | The channel's record in the vault |
bot_token | record | — | Bot/API token for the provider (see per-provider notes below) — set on the record |
signing_secret | record | — | Request verification secret (see per-provider notes below) — set on the record |
listen_channels | list | [] | Channel/chat IDs to accept inbound messages from (empty = all) |
post_channel | string | null | Default channel/chat ID for outbound messages (see per-provider notes below) |
verify_token | record | — | Webhook verification token — WhatsApp only (hub.verify_token) — set on the record |
Note: ChannelConfig allows extra fields (extra="allow") for any provider-specific config.
Per-Provider Field Semantics
| Provider | bot_token | signing_secret | post_channel | verify_token |
|---|---|---|---|---|
slack | Bot OAuth token (xoxb-…) | Slack signing secret (HMAC-SHA256) | Channel ID (e.g. C123456) | — |
telegram | Bot token from @BotFather | Secret token set when registering webhook | Chat ID | — |
discord | Bot token (without Bot prefix) | App public key (Ed25519 — requires pynacl) | Channel ID | — |
whatsapp | Meta Graph API access token | Meta app secret (HMAC-SHA256) | Phone number ID | hub.verify_token for webhook registration |
teams | Bot Framework App ID | Bot Framework App password | Conversation ID | — |
Inbound Route Registered Per Provider
| Provider | Route |
|---|---|
slack | POST /channels/slack/{agent_name}/events |
telegram | POST /channels/telegram/{agent_name}/webhook |
discord | POST /channels/discord/{agent_name}/interactions |
whatsapp | GET /channels/whatsapp/{agent_name}/webhook (verification) + POST (messages) |
teams | POST /channels/teams/{agent_name}/messages |
Example
channels:
slack:
connection: "slack-workspace" # Infrastructure → Messaging
listen_channels: ["C123456", "C789012"]
post_channel: "C123456"
telegram:
connection: "telegram-bot" # Infrastructure → Messaging
listen_channels: [] # empty = all chats
post_channel: ""
discord:
connection: "discord-bot" # Infrastructure → Messaging
listen_channels: ["987654321098765432"]
post_channel: "987654321098765432"
whatsapp:
connection: "whatsapp-business" # Infrastructure → Messaging
post_channel: "123456789012345" # phone number ID, not a phone number
teams:
connection: "teams-bot" # Infrastructure → Messaging
post_channel: "" # set at runtime from inbound activityEmail Channel
The email provider has a strict schema (validated at YAML load time, not at first send). Inbound is webhook-based via Mailgun / SendGrid / Postmark; outbound is SMTP via aiosmtplib.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
enabled | bool | true | no | Set false to keep the block but disable the adapter |
provider | string | "mailgun" | no | Inbound webhook provider: mailgun, sendgrid, postmark |
region | string | "us" | no | Provider region: us, eu (Mailgun: api.mailgun.net vs api.eu.mailgun.net) |
connection | string | null | yes | The email record (Infrastructure → Workspace → Email); holds the signing secret and SMTP host, port, username and password |
signing_secret | record | — | yes (inbound) | HMAC/ECDSA verification secret, on the record. Mailgun: HTTP webhook signing key. SendGrid: ECDSA P-256 verification key. Postmark: shared HMAC secret. |
inbound_domain | string | null | no | Domain hosting inbound mail — used for plus-addressing (agent+THREAD@inbound.example.com) |
inbound_local_part_filter | list | [] | no | Only process emails sent to these local parts (e.g. ["support", "help"]); empty = accept all |
auto_reply_drop | bool | true | no | Drop RFC 3834 auto-replies (Auto-Submitted, Precedence: bulk/list, noreply senders) |
smtp_host | record | — | yes (when enabled) | Outbound SMTP hostname |
smtp_port | record | 587 | no | SMTP port (587 STARTTLS, 465 implicit TLS) |
smtp_username | record | — | no | SMTP auth username |
smtp_password | record | — | no | SMTP auth password |
from_address | string | null | yes (when enabled) | Default From: address |
reply_to_address | string | null | no | Override Reply-To: header (defaults to from_address with plus-addressing token) |
use_starttls | bool | true | no | Negotiate STARTTLS for SMTP submission |
subject_prefix | string | null | no | Prefix for subjects, e.g. "[#TICKET-{thread_id}]" — used as fallback threading token when reference headers are stripped |
threading | string | "references_first" | no | Thread mapping strategy: references_first (prefer In-Reply-To/References headers, fall back to plus-addressing) or reply_to_token (skip headers, plus-addressing only) |
attachments | dict | see below | no | Inbound attachment handling — see [EmailAttachmentConfig](#emailattachmentconfig) |
EmailAttachmentConfig (under channels.email.attachments):
| Field | Type | Default | Description |
|---|---|---|---|
receive | bool | false | Opt-in: surface inbound attachments to the agent. Off by default for security. |
max_size_mb | int | 10 | Max accepted attachment size in MB (1–100) |
storage_url | record | — | Object-storage URL, set on the email record (e.g. s3://bucket/prefix). Agent receives a presigned URL — never raw bytes. |
Provider-specific signing notes:
| Provider | Verification | Notes |
|---|---|---|
mailgun | HMAC-SHA256 over timestamp + token | signing_secret = Mailgun's HTTP webhook signing key |
sendgrid | ECDSA P-256 | signing_secret = base64 ECDSA verification key (Inbound Parse settings) |
postmark | HTTP Basic Auth + IP allowlist | signing_secret = the shared HMAC secret; thread token comes from MailboxHash |
Inbound route: POST /channels/email/{agent_name}/{provider}/inbound
Required when enabled: the record's SMTP host and from_address must be set. Set enabled: false to register the block without configuring outbound.
Example:
agents:
support_agent:
name: "support_agent"
agent_type: "human"
channels:
email:
provider: "mailgun"
region: "us"
connection: "email-support" # Infrastructure → Workspace → Email: signing secret, SMTP host/port/user/password
inbound_domain: "inbound.example.com"
inbound_local_part_filter: ["support", "help"]
auto_reply_drop: true
from_address: "support@example.com"
subject_prefix: "[#TICKET-{thread_id}]"
threading: "references_first"
attachments:
receive: true
max_size_mb: 25 # the storage_url lives on the email recordSee Human Agents — Email Channel for end-to-end setup walkthroughs per provider.
Memory Config
Field: memory — accepts bool or dict.
Simple Mode
memory: false # Disabled (default)
memory: true # Enabled with defaultsAdvanced Mode (Dict)
| Field | Type | Default | Accepted Values | Description |
|---|---|---|---|---|
strategy | string | "recency" | recency, relevance, hybrid | Memory retrieval strategy |
limit | int | 10 | 1 – 100 | Max feed posts per invocation |
cross_session | bool | false | true, false | Persist memory across sessions |
cross_session_limit | int | 50 | 1 – 500 | Max cross-session posts to retain |
relevance_weight | float | 0.6 | 0.0 – 1.0 | Weight for relevance scoring |
recency_weight | float | 0.4 | 0.0 – 1.0 | Weight for recency scoring |
decay_hours | int | 24 | 1 – unlimited | Hours before entries decay |
Example
memory:
strategy: "hybrid"
limit: 10
cross_session: true
cross_session_limit: 50
relevance_weight: 0.6
recency_weight: 0.4
decay_hours: 24EscalationConfig
Used inside manager.escalation.
| Field | Type | Default | Description |
|---|---|---|---|
targets | list of [EscalationTarget](#escalationtarget) | [] | Escalation targets — all fire in parallel |
auto_escalate | dict | see below | Auto-escalation rules |
auto_escalate Fields
| Field | Type | Default | Description |
|---|---|---|---|
max_retries | int | 3 | Max retry attempts before escalating |
max_errors_per_session | int | 5 | Error count threshold per session |
timeout_threshold | int | 2 | Consecutive timeouts before escalating |
EscalationTarget
Each target in the escalation.targets list. A webhook target's address,
headers and token are an Outbound webhook record (an Add custom record
on Studio's Infrastructure page); the target names it with connection:. A
url or headers written inline is refused at load. The simplest escalation
is to an Inbox seat (human_interface: default):
targets: [{type: human_agent, agent: <seat>}].
| Field | Type | Default | Accepted Values | Applies To |
|---|---|---|---|---|
type | string | required | human_agent, webhook, channel | all |
agent | string | null | agent name | human_agent |
entry_point | string | null | entry point name | human_agent |
connection | string | null | an Outbound webhook record (Infrastructure page → Add custom): address, header and query rows | webhook |
method | string | "POST" | POST, PUT, PATCH | webhook |
payload_template | dict | null | JSON with {{var}} placeholders | webhook |
provider | string | null | slack, telegram, discord, whatsapp, teams | channel |
channel_id | string | null | channel ID | channel |
message_template | string | null | text with {{var}} placeholders | channel |
Example
escalation:
targets:
- type: "human_agent"
agent: "customer_support_team"
- type: "webhook"
connection: "incident-webhook" # Outbound webhook record: address + Authorization header row
method: "POST"
payload_template:
incident_id: "{{session_id}}"
severity: "{{severity_level}}"
message: "{{error_message}}"
- type: "channel"
provider: "slack"
channel_id: "#critical-incidents"
message_template: "Escalation: {{message}} (session: {{session_id}})"
auto_escalate:
max_retries: 3
max_errors_per_session: 5
timeout_threshold: 2BrokerConfig
Top-level brokers: is a registry of connections to external event sources. An agent's listen_events: references a broker by name. One broker connection serves any number of listeners on different topics/queues — same way Knative Brokers, Dapr Components, and Azure Functions Connection= work.
brokers:
prod_kafka:
type: "kafka"
connection: "prod-kafka" # record: bootstrap servers, SASL username and password
security_protocol: "SASL_SSL"
sasl_mechanism: "SCRAM-SHA-512"
ssl_cafile: "/etc/ssl/certs/kafka-ca.pem"
alerts_sqs:
type: "sqs"
connection: "alerts-sqs" # record: AWS keys and any endpoint
region: "us-east-1"
iot_mqtt:
type: "mqtt"
connection: "iot-mqtt" # record: host, port, username, password, PEMs
use_tls: true
external_redis:
type: "redis_streams"
connection: "upstream-redis" # record: the redis:// or rediss:// URL
db: 0
support_imap:
type: "imap"
connection: "support-imap" # record: host, port, username, password
use_tls: trueFrom 2.4.481 a broker's addresses and credentials are entered only in the
vault: make the record under Infrastructure → Brokers & queues (Redis
Streams, MQTT, Apache Kafka, AWS SQS or IMAP Mailbox) and name it with
connection:. The fields marked record below are set on that record; written
inline in YAML they are refused at load.
The type discriminator selects the schema. Each subtype has its own field set — typos and wrong types fail at SDK startup, not at first message.
KafkaBrokerConfig (type: "kafka")
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
type | string | "kafka" | yes | Discriminator |
connection | string | — | yes | The Apache Kafka record |
bootstrap_servers | record | — | yes | Kafka broker addresses, e.g. kafka-1:9092 |
security_protocol | string | unset | no | PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL |
sasl_mechanism | string | unset | no | PLAIN, SCRAM-SHA-256, SCRAM-SHA-512, GSSAPI |
sasl_username | record | — | no | SASL username |
sasl_password | record | — | no | SASL password |
sasl_kerberos_service_name, oauth_scope, aws_region, aws_role_arn | string | unset | no | Stay in YAML (not secrets). Like security_protocol and sasl_mechanism, set them only when the broker needs them. |
oauth_* credentials, PEM contents | record | — | no | Set on the record |
ssl_cafile | string | null | no | Path to CA bundle for TLS |
ssl_certfile | string | null | no | Client cert (mTLS) |
ssl_keyfile | string | null | no | Client key (mTLS) |
SQSBrokerConfig (type: "sqs")
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
type | string | "sqs" | yes | Discriminator |
connection | string | — | yes | The AWS SQS record |
region | string | — | yes | AWS region, e.g. "us-east-1" |
aws_access_key_id | record | — | no | Optional explicit key — otherwise IAM role credentials are used |
aws_secret_access_key | record | — | no | Optional explicit secret |
aws_session_token | record | — | no | STS session token (for assumed roles) |
endpoint_url | record | — | no | Override endpoint (LocalStack, VPC endpoints) |
role_arn, role_session_name, external_id | string | — | no | Assumed-role settings; stay in YAML |
MQTTBrokerConfig (type: "mqtt")
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
type | string | "mqtt" | yes | Discriminator |
connection | string | — | yes | The MQTT record |
host | record | — | yes | MQTT broker hostname |
port | record | 1883 | no | Port (1883 plain, 8883 TLS) |
username | record | — | no | MQTT username |
password | record | — | no | MQTT password (and any client PEMs) |
use_tls | bool | false | no | Enable TLS |
client_id | string | null | no | Client ID (random if unset) |
keepalive_s | int | 60 | no | Keepalive interval in seconds |
RedisStreamsBrokerConfig (type: "redis_streams")
Distinct from the SDK's primary Redis (used for sessions/yields/feed). Use this when an upstream service writes events into a Redis stream that an agent should consume.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
type | string | "redis_streams" | yes | Discriminator |
connection | string | — | yes | The Redis Streams record |
url | record | — | yes | redis://, rediss://, or unix:// URL |
db | int | 0 | no | Redis database number |
IMAPBrokerConfig (type: "imap")
For email-as-event-source when webhook-based email isn't viable (firewall, no public DNS). For webhook-based email channels on human agents, use channels.email instead.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
type | string | "imap" | yes | Discriminator |
connection | string | — | yes | The IMAP Mailbox record |
host | record | — | yes | IMAP server hostname |
port | record | 993 | no | Port (143 plain, 993 TLS) |
username | record | — | yes | IMAP username (usually full email address) |
password | record | — | yes | IMAP password or app-specific token |
use_tls | bool | true | no | Use implicit TLS (port 993) |
EventListener
A single entry in an agent's listen_events:. Each entry creates one long-lived asyncio task that consumes from the named broker and dispatches matching messages into the mesh as an entry-point event for the agent. All listeners use at-least-once delivery with idempotency keying on (listener_name, message_id) to prevent double-processing on retry.
agents:
order_processor:
name: "order_processor"
agent_type: "llm"
listen_events:
- broker: "prod_kafka"
topic: "orders.new"
group_id: "order-processor-v1"
filter:
type: "com.example.order.created"
deserialize: "myapp.schemas:OrderEvent"
delivery:
max_attempts: 5
backoff: "exponential"
backoff_initial_s: 2.0
backoff_max_s: 120.0
dead_letter:
broker: "prod_kafka"
topic: "orders.dlq"
batch_size: 10| Field | Type | Default | Required | Description |
|---|---|---|---|---|
broker | string | — | yes | Name of an entry in the top-level brokers: block |
topic | string | null | yes (Kafka) | Kafka topic to subscribe to |
group_id | string | null | no | Kafka consumer group ID (defaults to <sdk-name>-<agent-name>) |
queue | string | null | yes (SQS) | SQS queue name. A queue URL goes on the broker record instead |
stream | string | null | yes (Redis Streams) | Redis stream key |
consumer_group | string | null | no | Redis Streams consumer group |
mqtt_topic | string | null | yes (MQTT) | MQTT topic filter (supports + and # wildcards) |
qos | int | 1 | no | MQTT QoS level: 0 at-most-once, 1 at-least-once, 2 exactly-once |
folder | string | "INBOX" | no | IMAP folder to monitor |
poll_interval_s | float | null | no | Polling interval for IMAP / non-streaming sources |
filter | dict | null | no | CloudEvents-style attribute equality. AND-semantics — all key/value pairs must match for the listener to fire. |
deserialize | string | null | no | Pydantic class for payload validation, format "module.path:ClassName". ValidationError routes the message to DLQ. |
delivery | object | see [EventDeliverySpec](#eventdeliveryspec) | no | Retry + DLQ behavior |
batch_size | int | 1 | no | Messages fetched per poll cycle (1–1000) |
visibility_heartbeat | bool | false | no | (SQS only) Auto-extend the visibility timeout while a long-running agent handler executes |
EventDeliverySpec
| Field | Type | Default | Description |
|---|---|---|---|
max_attempts | int | 5 | Max delivery attempts before routing to DLQ (1–50) |
backoff | string | "exponential" | linear or exponential |
backoff_initial_s | float | 1.0 | Initial backoff delay in seconds |
backoff_max_s | float | 60.0 | Cap on backoff delay |
dead_letter | object | null | DLQ destination — see [DeadLetterRef](#deadletterref) |
DeadLetterRef
Points at another broker (by name) and the destination shape on it. Only one of topic/queue/stream is meaningful per broker type — the listener validates the right one is set at startup.
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
broker | string | — | yes | Name of a broker in the top-level brokers: block |
topic | string | null | when broker is Kafka | Kafka DLQ topic name |
queue | string | null | when broker is SQS | SQS DLQ queue name (a queue URL goes on the broker record) |
stream | string | null | when broker is Redis Streams | Redis Streams DLQ stream key |
How Listeners Fire Agents
When a message arrives:
- Verify — the listener verifies the source-specific signature/credentials at the connection layer.
- Filter —
filter:keys are matched against the normalized envelope. Non-match = ack and skip (no retry). - Idempotency — the runtime tracks
(listener_name, message_id)to drop duplicate deliveries. If already processed, ack and skip. - Deserialize — if
deserialize:is set, payload is validated through the named Pydantic class.ValidationError→ DLQ (no retry — bad data won't fix itself). - Wrap — message is wrapped in a CloudEvents-shaped
TriggerEventenvelope. - Dispatch — published as an
ENTRY_POINTevent for the agent. Same path as aPOST /webhook/{entry_point}. Agent counts toward invocation metering like any other invocation. - Ack — on success, the source is acked (Kafka offset commit, SQS
DeleteMessage, RedisXACK, etc.). - Retry / DLQ — on failure, the message is retried per
delivery.backoffuntilmax_attempts, then routed todead_letterif configured.
See Event Listeners — Architecture for an end-to-end walkthrough.
Top-Level Config (LeafMeshConfig)
| Field | Type | Default | Accepted Values | Description |
|---|---|---|---|---|
name | string | "default_mesh" | any string | Mesh name |
version | string | "1.0.0" | any string | Configuration version |
architecture | string | "managed_mesh" | managed_mesh | Architecture type (only one supported) |
debug | bool | false | true, false | Enable debug mode |
log_level | string | "INFO" | DEBUG, INFO, WARNING, ERROR | Logging level |
environment | string | "development" | development, production | Environment |
manager | object | see [ManagerConfig](#managerconfig) | — | Manager coordination + analysis |
mesh | object | see [MeshConfig](#meshconfig--cloud-providers) | — | Mesh network + cloud providers |
timeouts, scheduler, runtime, limits, connectors, security, self_healing | object | see [Runtime Behaviour Blocks](configuration#runtime-behaviour-blocks) | — | Runtime behaviour settings |
agents | dict | {} | agent name → AgentConfig | Agent configurations |
entry_points | list | [] (auto-populated with a default entry when none is provided) | see [Entry Points](#entry-points) | Named portals into mesh |
brokers | dict | {} | broker name → [BrokerConfig](#brokerconfig) | Top-level connection definitions referenced by agent.listen_events |
data_structures | dict | {} | name → DataStructure | Custom data type definitions |
auto_discover | dict | null | {"directory": "path", "pattern": "*.py", "recursive": true} | Auto-discover agent files |
evolution | object | see [EvolutionConfig](#evolutionconfig) | — | Evolutionary optimization (enabled defaults to false) |
evolution_api | object | see [EvolutionConfig](#evolutionconfig) | — | Evolution/health API (enabled, 127.0.0.1:19820 by default) |
Note: LeafMeshConfig has extra="forbid" — unknown top-level keys will raise a validation error.
From 2.4.482 the store is not a YAML setting: persistence: and top-level redis: blocks are no longer read (LeafMesh logs a warning at boot). Set the store in .env; see Store.
ManagerConfig
| Field | Type | Default | Accepted Values | Description |
|---|---|---|---|---|
enabled | bool | true | true, false | Enable manager analysis |
model | string | none — required when enabled | same as [Model List](#model-list) | LLM model for Manager analysis. No default since 2.4.135; a manager declared without one is invalid, and supervision is reported INERT at boot rather than silently running on a model you did not choose. |
domain | string | "generic" | generic, ecommerce, data_analysis | Manager domain specialization |
prompt | string | null | any string (multiline supported) | Evaluation criteria — tell the Manager what success looks like, what to escalate on, and what patterns to watch. Folded into every Manager analysis pass alongside the domain guidance. |
can_intervene | bool | true | true, false | Allow manager interventions (false = read-only) |
coordination_rules | dict | {} | arbitrary key-values | User-defined business rules (max_call_depth, agent_rules, reflection_budget (default 2), max_retries, ...). Not the place for the four rule sets below. |
human_input_rules | dict | see below | — | Limits on human-input requests. Directly under manager: |
timeout_rules | dict | see below | — | Timeout escalation. Directly under manager: |
workflow_pause_rules | dict | see below | — | Paused-workflow limits. Directly under manager: |
human_response_rules | dict | see below | — | What happens after each kind of human reply. Directly under manager: |
chain_completion_timeout | float | 1800.0 | seconds | Wait time before checking chain completeness |
health_check_interval | int | 60 | seconds | Seconds between health checks |
agent_timeout_threshold | int | 180 | seconds | Seconds before agent is timed out |
escalation | object | null | see [EscalationConfig](#escalationconfig) | Escalation targets and rules |
routing | dict | see below | — | Manager routing configuration |
manager.prompt — Evaluation Criteria
Gives the Manager direct context about your mesh's specific purpose, success criteria, and escalation triggers. The Manager reads this on every agent turn alongside its domain guidance.
manager:
model: "gpt-4o-mini"
domain: "generic"
prompt: |
This mesh handles customer support tickets.
A successful flow means:
- greeter identifies the issue category correctly
- processor routes to the right specialist agent
- the customer receives a clear resolution within 5 minutes
Escalate if:
- the same issue loops more than twice
- sentiment is negative AND no resolution has been proposed
- the human agent times out without responding
Watch for:
- advisor_agent confidence scores below 0.6
- processor_agent routing to fallback more than 50% of the timerouting Fields
| Field | Type | Default | Accepted Values | Description |
|---|---|---|---|---|
mode | string | "static" | static, learning | Routing mode (static=YAML only, learning=adaptive) |
memory_size | int | 100 | 1 – 1000 | Max routing decisions to remember |
confidence_threshold | float | 0.7 | 0.0 – 1.0 | Min confidence to accept learned route |
fallback | string | "all" | all | Fallback when confidence too low |
decay_days | int | 30 | 1 – 365 | Days before old routing memory decays |
The four rule sets below live directly under manager: (for example manager.timeout_rules), never under manager.coordination_rules:. From 2.4.482 a file that still nests them there is moved at load, with a warning.
human_input_rules (Defaults)
| Field | Type | Default |
|---|---|---|
max_concurrent_requests | int | 3 |
max_agent_requests | int | 5 |
enable_request_queuing | bool | true |
timeout_rules (Defaults)
| Field | Type | Default |
|---|---|---|
max_timeouts_before_escalation | int | 2 |
timeout_escalation_enabled | bool | true |
escalation_notify_managers | bool | true |
workflow_pause_rules (Defaults)
| Field | Type | Default |
|---|---|---|
max_pause_duration_minutes | int | 30 |
max_concurrent_paused_workflows | int | 2 |
pause_monitoring_enabled | bool | true |
human_response_rules (Defaults)
| Scenario | requires_manager_review | auto_escalate |
|---|---|---|
approval | false | false |
escalation | true | true |
timeout | true | false |
The scenario keys name the kind of reply a human agent sent (type on the response). They decide whether the manager reviews or escalates after that reply; they are not an approval step. Approval itself is routing — see the removed-fields note under Human Agent Fields above.
MeshConfig & Cloud Providers
MeshConfig
| Field | Type | Default | Description |
|---|---|---|---|
reply_stream | bool | false | Route mesh replies through the per-session reply stream (opt-in; always on in multi-worker mode) |
bedrock | object | null | AWS Bedrock config |
vertex | object | null | Google Vertex AI config |
foundry | object | null | Microsoft Foundry/Azure AI config |
local | object | null | Local model server config (vLLM, SGLang, Ollama, etc.) |
mesh.call_timeout is gone. The default mesh call timeout is timeouts.mesh_call_default_timeout_s (default 300 seconds); from 2.4.482 an old mesh.call_timeout is moved there at load, with a warning.
From 2.4.481 the address and credentials of bedrock, vertex, foundry and local are entered only in the vault, under Infrastructure → Models (a local server under Models → Local / self-hosted); each block names its record with connection:. Fields typed record below are set on that record; written inline they are refused at load. Model keys and LOCAL_MODEL_ENDPOINT in .env still work: a value in .env wins, and the vault fills any gaps.
BedrockConfig (AWS)
| Field | Type | Default | Description |
|---|---|---|---|
connection | string | null | The "AWS Bedrock" record under Infrastructure → Models |
region | record | "us-east-1" | AWS region |
profile | string | null | AWS profile from ~/.aws/credentials |
endpoint_url | record | — | Custom Bedrock endpoint URL |
Auth: AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY env vars, IAM role, or ~/.aws/credentials
VertexConfig (Google Cloud)
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
connection | string | null | yes | The "Google Vertex AI" record under Infrastructure → Models |
project | record | — | yes | GCP project ID |
location | record | "us-central1" | no | GCP region |
Auth: GOOGLE_APPLICATION_CREDENTIALS env var or gcloud auth
FoundryConfig (Azure)
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
connection | string | — | yes | The "Azure AI Foundry" record under Infrastructure → Models |
endpoint | record | — | yes | Foundry endpoint URL (e.g. https://<resource>.openai.azure.com) |
api_version | string | null | no | Azure API version (e.g. 2024-10-21). Omit for v1 endpoint. |
Auth: AZURE_FOUNDRY_API_KEY or AZURE_FOUNDRY_TOKEN env vars
LocalModelConfig
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
connection | string | null | no | The record under Infrastructure → Models → Local / self-hosted |
endpoint | record | "http://localhost:11434/api/generate" | no | Model server URL |
server_type | string | null (auto-detect) | no | Server type: openai, ollama, textgen, huggingface |
timeout | float | 120.0 | no | Request timeout in seconds for the local model (local models can be slow on CPU) |
headers | record | {} | no | Custom HTTP header rows (e.g. for auth) |
LOCAL_MODEL_ENDPOINT and LOCAL_MODEL_SERVER_TYPE are read from .env, or filled from a Local / self-hosted model record. A local model's request timeout is mesh.local.timeout (default 120 s). With no mesh.local block, the local model uses timeouts.llm_hard_timeout_s (default 90 s).
Supported servers and their endpoint + server_type:
| Server | server_type | Example endpoint |
|---|---|---|
| vLLM | openai | http://localhost:8000/v1/chat/completions |
| SGLang | openai | http://localhost:30000/v1/chat/completions |
| llama.cpp server | openai | http://localhost:8080/v1/chat/completions |
| LM Studio | openai | http://localhost:1234/v1/chat/completions |
| LocalAI | openai | http://localhost:8080/v1/chat/completions |
| TGI (--api openai) | openai | http://localhost:8080/v1/chat/completions |
| TensorRT-LLM | openai | http://localhost:8000/v1/chat/completions |
| Triton + vLLM | openai | http://localhost:8000/v1/chat/completions |
| Ollama | ollama | http://localhost:11434/api/generate |
| Text Gen Web UI | textgen | http://localhost:5000/api/v1/generate |
| HF Inference | huggingface | https://api-inference.huggingface.co/models/... |
Tip: Most production servers (vLLM, SGLang, TGI, llama.cpp) expose OpenAI-compatible endpoints. Use
server_type: openaifor all of them. Ifserver_typeis omitted, it's auto-detected from the URL.
mesh:
local:
connection: "vllm-local" # record: endpoint http://localhost:8000/v1/chat/completions + Authorization header row
server_type: openai
timeout: 120 # secondsStore (.env, not YAML)
From 2.4.482 the store is set only in .env. There is no persistence: or redis: block in the YAML.
| Variable | Default when unset | What it sets |
|---|---|---|
LEAFMESH_STORE | redis | redis or postgres |
| Redis | ||
REDIS_URL | none | The whole server in one URL, e.g. redis://user:pw@host:6379/4. rediss:// turns TLS on. The single names below fill in anything it leaves out. |
REDIS_HOST | localhost | Server host |
REDIS_PORT | 6379 | Server port |
REDIS_PASSWORD | none | Server password |
REDIS_DB | 0 | Database number |
REDIS_DEFAULT_TTL | 3600 (1 hour) | Lifetime of general data (yields, mesh communications), seconds |
REDIS_SESSION_TTL | 604800 (7 days) | Lifetime of sessions and conversation history, seconds |
REDIS_SSL | false | Set true to require TLS |
REDIS_SSL_CERT_REQS | required | Certificate check: required, optional or none |
REDIS_SSL_CA_CERTS | none | Path to the CA bundle |
REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILE | none | Client certificate and key (mutual TLS) |
REDIS_SSL_CHECK_HOSTNAME | true | Check the server hostname against its certificate |
REDIS_CLUSTER_NODES | none | Comma-separated host:port list. Setting it turns on Redis Cluster mode. |
| Postgres | ||
DATABASE_URL | none | The whole server in one URL, e.g. postgresql://user:pw@host:5432/db?sslmode=require. The single names below fill in anything it leaves out. |
POSTGRES_HOST | localhost | Server host |
POSTGRES_PORT | 5432 | Server port |
POSTGRES_DB | leafmesh | Database name |
POSTGRES_USER, POSTGRES_PASSWORD | none | Credentials |
POSTGRES_SSLMODE | unset (the driver's own default) | Postgres SSL mode |
POSTGRES_DEFAULT_TTL | 3600 (1 hour) | Lifetime of general data, seconds |
POSTGRES_SESSION_TTL | 604800 (7 days) | Lifetime of sessions, seconds |
POSTGRES_POOL_MIN | 1 | Smallest connection pool size |
POSTGRES_POOL_MAX | 10 | Largest connection pool size |
Studio shows under Infrastructure → Datastores whether the store is reachable, its latency and server, the settings in force and which .env names set them; it never shows the address or password. See Persistence.
APIConfig
Top-level api: block Configures the LeafMesh HTTP server.
| Field | Type | Default | Description |
|---|---|---|---|
cors_origins | list[string] | no fixed default (empty until listed) | Additional CORS origins, appended to the built-in defaults (https://platform.leafcraft.ai + localhost dev ports). Each entry must be a full origin (scheme://host[:port]). |
EvolutionConfig
evolution: and evolution_api: are current top-level YAML blocks. A new
agency's YAML is written with these values:
evolution:
enabled: false # default false
strategy: genetic
population_size: 20
generations: 50
mutation_rate: 0.1
crossover_rate: 0.7
elite_size: 2
mutation_types:
- agent_connections
- communication_patterns
- agent_parameters
- workflow_sequences
- resource_allocation
- timeout_settings
fitness_function: task_completion_rate
selection_method: tournament
evolution_api:
enabled: true # default true
host: 127.0.0.1 # default
port: 19820 # default| Field | Default | Description |
|---|---|---|
evolution.enabled | false | Turn evolution on |
evolution_api.enabled | true | Run the evolution/health API |
evolution_api.host | 127.0.0.1 | Address the evolution/health API listens on |
evolution_api.port | 19820 | Port of the evolution/health API (LEAFMESH_EVOLUTION_API_PORT overrides it) |
Changing evolution_api.host, port or enabled needs a process restart;
every other saved setting applies without one (from 2.4.484).
Operators kick off evolution runs from Studio with their test scenarios. See Evolutionary Optimization for what evolution does and how to use it.
DataStructure
| Field | Type | Default | Description |
|---|---|---|---|
type | string | required | object, string, number, boolean, list |
properties | dict | null | Object properties (for type=object) |
required | list | [] | Required field names |
validation_rules | dict | {} | Validation rules (key-value strings) |
Entry Points
Each entry point is a named portal into the mesh.
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Entry point name |
target | string | required | Target agent name |
description | string | optional | Description |
condition | string | "always" | Trigger condition |
Example
entry_points:
- name: "greet_user"
target: "greeter_agent"
description: "Main entry — greets user and starts the agent chain"
- name: "direct_research"
target: "researcher_agent"
description: "Skip greeting, go straight to research"Validation Rules
These are enforced at config load:
| Rule | Constraint |
|---|---|
agent_type | Must be llm, human, programmatic, or external |
communication_type | Must be dual, chain, or execute |
optimization_strategy | Must be performance, cost, or speed (or null) |
max_tool_calls_per_message | Must be 0–20 |
tool_call_timeout | Must be > 0 and <= 300 seconds |
tool_choice | Must be a non-empty string (auto, none, or tool name) |
framework | Must be one of the supported frameworks listed under [External Agent Fields](#external-agent-fields-agent_type-external) (or null). voiceflow and cognigy are refused as removed |
framework required | When agent_type is external, framework must be set |
integration | Must be zapier, composio, n8n, mcp, python (2.4.492+) (or null) |
integration restricted | Only valid when agent_type is programmatic |
is_human_powered sync | Auto-set to true when agent_type="human", forced to false when agent_type="llm" |
AgentConfig extras | Allows arbitrary extra fields (extra="allow") |
LeafMeshConfig extras | Rejects unknown top-level keys (extra="forbid") |
Field Applicability by Agent Type
Shows which fields are used (U), ignored (—), or required (R) for each agent type. thinking_budget is a deprecated alias: it is read as reasoning_budget, with a warning, and is not a field of its own.
| Field | llm | human | programmatic | external |
|---|---|---|---|---|
name | R | R | R | R |
description | U | U | U | U |
model | U | — | — | — |
model_connection | U | — | — | — |
prompt | U | — | — | — |
temperature | U | — | — | — |
max_tokens | U | — | — | — |
max_completion_tokens | U | — | — | — |
reasoning | U | — | — | — |
thinking | U | — | — | — |
sandbox | U | — | — | — |
framework_guidance | U | — | — | — |
image_options | U | — | — | — |
super_agent | U | — | — | — |
reasoning_budget | U | — | — | — |
enable_prompt_caching | U | — | — | — |
optimization_strategy | U | — | — | — |
context_parts | U | — | — | — |
tools | U | — | — | — |
tool_choice | U | — | — | — |
max_tool_calls_per_message | U | — | — | — |
tool_call_timeout | U | — | — | — |
allow_parallel_tool_calls | U | — | — | — |
tool_categories | U | — | — | — |
is_human_powered | — | auto | — | — |
human_interface | — | U | — | — |
human_timeout_seconds | — | U | — | — |
human_context_template | — | U | — | — |
human_prompt_template | — | U | — | — |
fallback_on_timeout | — | U | — | — |
fallback_response | — | U | — | — |
webhook_config | — | U | — | — |
channels | — | U | — | — |
framework | — | — | — | R |
connector_config | — | — | U | U |
request_form | — | — | U | U |
price_per_1k_tokens | — | — | U | U |
integration | — | — | U | — |
communication_type | U | U | U | U |
notify | U | U | U | U |
scaling | U | U | U | U |
wake_up | U | U | U | U |
yields | U | U | U | U |
inputs | U | U | U | U |
can_call | U | U | U | U |
narration | U | U | U | U |
knowledge | U | — | — | — |
wait_for | U | U | U | U |
wait_for_timeout | U | U | U | U |
auto_store_response | U | U | U | U |
auto_store_yields | U | U | U | U |
memory | U | — | — | — |
Legend: R = required, U = used, — = ignored, auto = auto-set