Agent Config Fields

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

  1. Agent Types
  2. AgentConfig — Core Fields (All Types)
  3. LLM Agent Fields
  4. Human Agent Fields
  5. External Agent Fields
  6. Programmatic Agent Fields
  7. WebhookConfig
  8. ChannelConfig
  9. Memory Config
  10. EscalationConfig
  11. EscalationTarget
  12. BrokerConfig
  13. EventListener
  14. Top-Level Config (LeafMeshConfig)
  15. ManagerConfig
  16. MeshConfig & Cloud Providers
  17. Store (.env, not YAML)
  18. EvolutionConfig
  19. DataStructure
  20. Entry Points
  21. Validation Rules
  22. Field Applicability by Agent Type

Agent Types

ValueDescription
llmLLM-powered agent (default). Executes via OpenAI, Claude, Bedrock, Vertex, or Foundry.
humanHuman operator agent. Routes to a person via API, webhook, or channel.
programmaticPython function with business logic. No LLM calls.
externalDelegates to an external framework (CrewAI, LangGraph, AutoGen, etc.).

AgentConfig — Core Fields (All Types)

These fields apply to every agent regardless of agent_type.

FieldTypeDefaultAccepted ValuesRequiredDescription
namestring—any stringyesUnique agent name within the mesh
descriptionstringnullany stringnoAgent description and purpose
agent_typestring"llm"llm, human, programmatic, externalnoAgent execution type (see note below)
communication_typestring"dual"dual, chain, executenoHow agent communicates with the mesh
response_overrideslist or dictnulllist of {caller, type} OR dict {caller_name: type}noPer-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).
scalingobject—runs, copiesnoHow 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)
notifydictnull{webhook, when}no2.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_onlyboolfalsetrue, falseno2.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_upstring or listnullcron string; OR (2.4.107+) a list of cron strings; OR a list of {cron, input} entriesnoScheduled 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_inputdictnullany JSON objectno2.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
yieldsdict{}key: field name, value: type string or nested objectnoOutput 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
inputsdict{}key: field name, value: type string or nested objectnoInput schema — what agent expects
can_calllist[]list of {"agent": "name"} or {"agent": "name", "condition": "expr"}noAgents this agent can invoke
listen_eventslist[]list of [EventListener](#eventlistener) entriesnoExternal 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.
narrationstringnullany string (multiline supported)noPlain-English routing hints for the Manager — evaluated when conditions don't cover everything (see [Narration Routing](#narration-routing))
wait_forstring or list[]agent names or expression stringnoFan-in/join condition
wait_for_timeoutint601 – unlimited (seconds)noHard timeout for fan-in
auto_store_responsebooltruetrue, falsenoAuto-store responses in Redis
auto_store_yieldsbooltruetrue, falsenoAuto-store yields in Redis
memorybool or dictfalsetrue, false, or memory config dictnoAgent memory (LLM agents) — see [Memory Config](#memory-config)
memory_limitint101 – 100noLegacy: max recent feed posts (use memory.limit instead)
knowledgebool or dictfalsefalse, or {serviceName, enabled, groupName}noKnowledge/RAG (LLM agents) — see [Knowledge Config](#knowledge-config)
enforce_yieldsboolfalsetrue, falsenoStrictly 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_retryint00 – unlimitednoMaximum 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_historyboolfalsetrue, falsenoOpt-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_limitint201 – 200noMax 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_yieldstringnullany yield key from yields:noFor 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:

  1. If response_overrides[called_by] is set → use that type.
  2. 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 reply

Dict form (also accepted, terser):

agents: sales_agent: name: "sales_agent" communication_type: chain response_overrides: customer_facing_agent: dual escalation_router: dual

Both 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_call conditions 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_retry attempts. 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_context is added to the workflow payload.
    • Programmatic agents — input_data._rerun_context is available alongside the (possibly auto-corrected) inputs.
  • After the retry budget is exhausted, fires AGENT_ERROR with error_type="YieldContractFailure" + retry_exhausted=true. Routes through manager.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 escalating

Programmatic 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.

KeyUsed byEffect
_target_operator (recommended)All human-agent pathsLogical 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_idsDefault inboxList override — narrows the HITL broadcast to listed operators. Legacy convention, still works.
_target_channel_idChannel adaptersEscape 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_providerChannel adaptersPin outbound to one provider when several configured. Values: slack, discord, telegram, whatsapp, teams, email.
_target_webhook_urlWebhook fallbackRaw 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:

  1. _target_channel_id (raw override — power user / dynamic ID)
  2. _target_operator → channels.<provider>.operators[operator] (ergonomic)
  3. adapter.get_channel_for_session(session_id) (reply continuity — the channel the inbound message arrived from)
  4. adapter.config["post_channel"] (YAML default)

For the webhook fallback path:

  1. _target_webhook_url (raw)
  2. webhook_config.operators[operator] (ergonomic)
  3. The webhook_config record'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

ScenarioUse
Fixed recipient — always the same personYAML 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

TransportSupports _target_operator viaRaw override key
Default inbox(narrows broadcast to that operator)_operator_ids
Slackchannels.slack.operators_target_channel_id + _target_channel_provider: slack
Discordchannels.discord.operators_target_channel_id + _target_channel_provider: discord
Telegramchannels.telegram.operators_target_channel_id + _target_channel_provider: telegram
WhatsAppchannels.whatsapp.operators_target_channel_id + _target_channel_provider: whatsapp
Teamschannels.teams.operators_target_channel_id + _target_channel_provider: teams
Emailchannels.email.operators_target_channel_id + _target_channel_provider: email
Webhook fallbackwebhook_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 own connector_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 typeField
programmaticcontext["_conversation_history"]
external (connector)input_data["_conversation_history"] AND context["_conversation_history"]
humanincluded 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: object
prompt: ... 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 declaresexactly one yield
Type isobject (or dict)
LLM returneda 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

SymptomCauseFix
Condition silently fails, narration routes elsewhereLLM returned a different shape than declared in yieldsEither 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 logsThe yield key was missing → defaulted to None/{} → dot-path access against NoneCheck the YIELDS: ... produced incomplete output warning that fires just before — that names the missing key
Condition evaluates to False when you expected TrueYield 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
llmmodel, prompt, temperature, max_tokens, max_completion_tokens, thinking, reasoning, reasoning_budget, tools, tool_categories, context_parts, tool_choice
humanwebhook_config, human_interface, human_timeout_seconds, channels, is_human_powered
externalframework, connector_config
programmaticintegration

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 StringDescription
"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: object

When 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 expression

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

  1. Conditions are evaluated first by the control plane (AST, instant, deterministic)
  2. Condition targets are dispatched immediately
  3. The Manager's analysis pass reads the narration alongside the agent output
  4. The Manager recommends additional next agents based on both condition results and narration hints
  5. 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"
FieldTypeRequiredDefaultDescription
serviceNamestringyes—The knowledge service's name (its store and embeddings are set up once for the mesh, not per agent)
enabledboolnotruefalse keeps the setting and withholds the knowledge tool — the agent stops querying it
groupNamestringnonullQuery 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_knowledge tool 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.

FieldTypeDefaultAccepted ValuesDescription
modelstringnone — requiredsee [Model List](#model-list) belowLLM 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_connectionstringnulla vault record name2.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.
promptstringnullany string (multiline supported)System prompt
temperaturefloat0.10.0 – 2.0LLM temperature (creativity/randomness)
max_tokensint8001 – model maxMax output tokens (legacy models)
max_completion_tokensintnull1 – model maxMax completion tokens (o-series, GPT-5.x and GPT-6 models)
thinkingboolfalsetrue, falsev2.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.
sandboxbool or dictofftrue, 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_guidancebooltruetrue, falseAdd 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_optionsdictnullprovider-specific keysOptions 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.
reasoningboolfalsetrue, falsev2.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_budgetintnull1024 – 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_cachingboolfalsetrue, falseEnable provider-native prompt caching for cost reduction (see below)
cacheboolnulltrue, falseLLM-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))
effortstringnulllow, medium, highReasoning-effort hint for models that support it — depth vs. latency/cost trade
skillslist[]skill namesSkill packs loaded into this agent's prompt context (LLM agents only)
optimization_strategystringnullperformance, cost, speedPer-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_partsdictnullsee belowOptional context parts
toolslist[]tool name stringsAvailable tools
tool_choicestring"auto"auto, none, or specific tool nameTool selection strategy
max_tool_calls_per_messageint50 – 20Max tool calls per LLM message
tool_call_timeoutfloat30.00.1 – 300 (seconds)Tool execution timeout
llm_hard_timeout_sfloatnull> 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_callsbooltruetrue, falseAllow 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_categorieslist[]category name stringsTool categories agent can access
strict_toolsboolfalsetrue, falseOpt into provider strict tool schemas (the model cannot pass hallucinated/extra arguments).
super_agentbool or dictfalsetrue, false, or a tuning mapWrap 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_strategystringnullrecency, relevant, hybridHow prior activity is selected for recall when memory is enabled.
knowledge_strategystringnullinject, tool, bothWhether configured knowledge is pre-injected into the prompt, offered as the query_knowledge tool, or both.
monthly_token_budgetintnull> 0Per-agent monthly token budget. When exceeded, the agent stops per budget_on_exhaust. Opt-in — unset means no cap.
monthly_cost_budgetfloatnull> 0 (USD)Per-agent monthly cost budget. When exceeded, the agent stops per budget_on_exhaust. Opt-in.
budget_on_exhauststring"graceful"graceful, escalateOn budget exhaustion: graceful fills declared yields with a budget-exhausted marker and ends cleanly; escalate routes to the Manager error path.
schedule_targetslistnullagent name stringsAgents this agent may target with [schedule_followup](../tools/schedule-followup). Defaults to the agent's can_call set.
max_followup_depthint5> 0Max depth of self-scheduled follow-up chains (continuations of continuations) before the tool refuses.
max_followups_per_sessionint20> 0Max 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]).

KeyLabel injectedDescription
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-luna
  • gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna
  • gpt-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-nano
  • gpt-4.1, gpt-4.1-mini, gpt-4.1-nano, gpt-4o, gpt-4o-mini
  • o3, o4-mini
  • GPT-6, GPT-5.6 and the -pro models call tools through OpenAI's Responses API; LeafMesh switches to it automatically.

Anthropic: (ANTHROPIC_API_KEY)

  • claude-fable-5-1, claude-fable-5
  • claude-opus-5-5, claude-opus-5, claude-sonnet-5-5, claude-sonnet-5, claude-haiku-5-5
  • claude-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-lite
  • gemini-3.1-pro-preview, gemini-3.1-flash-lite, gemini-3-flash-preview
  • gemini-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-chat and deepseek-reasoner are 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-12
  • codestral-2508
  • Self-hosted: set MISTRAL_BASE_URL to override the default https://api.mistral.ai/v1 endpoint

xAI Grok: (XAI_API_KEY / GROK_API_KEY) (v2.2.25)

  • grok-4.7, grok-4.6, grok-4.5, grok-4.3
  • grok-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.bedrock config

Google Vertex AI:

  • Any Vertex model ID (e.g. gemini-2.5-pro)
  • Requires mesh.vertex config

Azure Foundry:

  • Any Azure deployment name
  • Requires mesh.foundry config

Local Models (vLLM, SGLang, Ollama, llama.cpp, etc.):

  • Any model name supported by your local server
  • Requires mesh.local config (or LOCAL_MODEL_ENDPOINT env 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.

Featurethinking: truereasoning: true
What it doesSDK adds three text tools (chain_of_thought, metacognitive_reflection, apply_edits) to the available tool list. It never runs code — that is sandboxEnables provider-native extended reasoning / thinking
Works withAny model (tool injection)Only models that support native reasoning
Token costTool-call overheadDedicated reasoning tokens (billed as output)
QualityGood for structured CoT scaffoldingBest 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

ProviderModel RequirementBehavior
AnthropicClaude 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.
OpenAIo-series (o3/o4-mini), GPT-5.x, GPT-6A 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.
GoogleGemini 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.
DeepSeekThe 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.
BedrockClaude models on Bedrock (same Anthropic rules)Writes additionalModelRequestFields.thinking (NOT inferenceConfig.thinking — that location is rejected).
VertexClaude + Gemini on VertexBoth paths supported with the same family rules.
FoundryAzure o-series + gpt-5.x deploymentsreasoning_effort: str (same kwarg shape as direct OpenAI).
Mistral *(v2.2.25)*No native reasoning API as of v2.2.25reasoning: 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.
LocalDepends on model/serverPassthrough if server supports it.

Prompt Caching — Provider Support

ProviderHow it worksSavings
Anthropiccache_control: ephemeral on system prompt + tools~90% on cached reads
BedrockpromptCaching parameter~90% on cached reads
Vertex (Claude)cache_control: ephemeral on system prompt~90% on cached reads
OpenAIAutomatic — no config needed (stats in response)~50% on cached
GoogleContext 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
ProviderWhat the SDK applies under the hood
OpenAIresponse_format = {"type": "json_object"} (basic JSON mode)
DeepSeekresponse_format = {"type": "json_object"}
Foundryresponse_format = {"type": "json_object"}
Localresponse_format = {"type": "json_object"}
AnthropicTool-based JSON extraction using the derived schema
BedrockSchema injected into system prompt
Google / Vertexresponse_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.

FieldTypeDefaultAccepted ValuesDescription
is_human_poweredboolfalsetrue, falseAuto-synced to true when agent_type="human"
human_interfacestring"api"default, api, webhook, customHow human receives/submits input (see below)
human_timeout_secondsint3001 – 3600 (seconds)Human response timeout
human_context_templatestringnullany stringTemplate for presenting context to human
human_prompt_templatestringnullany stringTemplate for human prompts
fallback_on_timeoutboolfalsetrue, falsev2.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_responsedictnullarbitrary JSONDefault response on human timeout
operator_idslist[]list of strings (email or ID)Operators who can see this agent's HITL requests. Empty = broadcast (all operators see it).
webhook_configobjectnullsee [WebhookConfig](#webhookconfig)Webhook settings (required for webhook interface)
channelsdict{}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_agent

human_interface — Interface Types

ValueDescriptionHow it works
defaultBuilt-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.
webhookExternal webhookPOSTs 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.).
apiPython callbackCalls 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.
customCustom handlerSame as api — uses the registered human_interface_handler callback.

Note: default mode 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, point webhook at your own service through an Outbound webhook record or use api with 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: string

Example — 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".

FieldTypeDefaultAccepted ValuesRequiredDescription
frameworkstringnullcrewai, 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, customyesExternal 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_configdict{}framework-specific key-valuesnoConnection 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_formdict{}up to 50 keys (1–64 of A-Z a-z 0-9 _ - ., not starting with _), 16 KB of JSON in all; no secretsnoYour 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_config values to call different workflows.

All connector_config fields can also be overridden per-call via request.connector_config at 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:

FieldTypeDefaultDescription
connectionstring—Vault connection that supplies this connector's credential and account fields (on Zapier, use vault_connection — its connection is the app)
after_handoffstring"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_timeoutfloat120.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

FieldTypeDefaultRequiredDescription
endpointrecord""yesHTTP endpoint for deployed CrewAI crew
api_keyrecord""noBearer Token for authentication
user_api_keyrecord""noUser Bearer Token (preferred over api_key when both are set)
poll_intervalfloat2.0noSeconds between status polls
max_poll_secondsfloat300.0noMax total polling time (seconds)
http_timeoutfloat30.0noHTTP request timeout (seconds)

langgraph

FieldTypeDefaultRequiredDescription
endpointrecord""yesLangGraph Platform deployment URL
api_keyrecord""noAPI key
graph_idstring"agent"noWhich graph to run — this is the workflow selector
poll_intervalfloat1.0noSeconds between status polls
max_poll_secondsfloat300.0noMax total polling time (seconds)
http_timeoutfloat30.0noHTTP request timeout (seconds)

autogen

Connects to an external AutoGen Studio or custom AutoGen API service via HTTP.

FieldTypeDefaultRequiredDescription
endpointrecord""yesAutoGen service base URL (e.g. http://localhost:8081)
api_keyrecord""noBearer token for authentication
workflow_idstring""noWorkflow/agent ID to execute on the AutoGen service
timeoutfloat120.0noHTTP request timeout (seconds)
poll_intervalfloat2.0noSeconds between status poll requests
max_poll_secondsfloat300.0noMax total polling time (seconds)

a2a

FieldTypeDefaultRequiredDescription
urlrecord""yesA2A-compatible agent server base URL
auth_tokenrecord""noBearer token for authentication
auth_schemestring"Bearer"noAuthorization header scheme
poll_intervalfloat2.0noSeconds between task status polls
max_poll_secondsfloat300.0noMax total polling time (seconds)
http_timeoutfloat30.0noHTTP request timeout (seconds)

mcp

MCP supports two transport modes. tool_name is always required.

Common fields (both transports):

FieldTypeDefaultRequiredDescription
tool_namestring""yesWhich MCP tool to call — the workflow selector
transportstring"stdio"noTransport mode: "stdio" or "http"
timeoutfloat60.0noRequest timeout (seconds)

stdio transport fields:

FieldTypeDefaultRequiredDescription
commandstring""yes (stdio)Executable to launch (e.g. "npx")
argslist[]noCommand arguments (e.g. ["-y", "@mcp/server-npm"])
envdictnullnoEnvironment variables for the subprocess

http transport fields:

FieldTypeDefaultRequiredDescription
urlrecord""yes (http)MCP server HTTP/SSE endpoint
auth_tokenrecord""noBearer token

zapier

Tool name is built as {connection}_{action} (e.g. google_sheets_create_row).

FieldTypeDefaultRequiredDescription
connectionstring""yes*Zapier app name (e.g. "google_sheets", "slack", "gmail")
actionstring""yes*Action name (e.g. "create_row", "send_message")
mcp_keyrecord""yesZapier MCP key, on the record
vault_connectionstring—yesThe Zapier record holding mcp_key (and any mcp_endpoint override)
mcp_endpointrecordZapier's MCP servernoOverride the Zapier MCP address
timeoutfloat60.0noHTTP 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

FieldTypeDefaultRequiredDescription
actionstring""yesComposio action enum (e.g. "GITHUB_STAR_A_REPOSITORY") — the workflow selector
entity_idstring"default"noUser/entity context for managed auth
api_keyrecord""noComposio API key
timeoutfloat60.0noExecution timeout (seconds)

n8n

FieldTypeDefaultRequiredDescription
webhook_urlrecord""yesFull webhook trigger URL — one URL per n8n workflow
auth_tokenrecord""noBearer token
api_keyrecord""noThe 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_urlrecordthe webhook's hostnoThe instance address, when the n8n API is not on the webhook's host
timeoutfloat60.0noHTTP 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:

  1. Start with a Webhook trigger node (receives the payload including _leafmesh_callback_url)
  2. Configure the Webhook node to "Respond Immediately" (optional — reply works too)
  3. Process the workflow
  4. 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.

FieldDefaultFrameworksDescription
after_handoff"wait"all fourwait or move_on; reply is refused (a platform never answers in the reply)
callback_timeout1800all fourSeconds allowed for the call's outcome
http_timeout30all fourTimeout for the request that starts the call (seconds)
agent_id—elevenlabs_agents (required), retell, blandKept 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), blandThe number the call is made from (E.164)
pathway_id, persona_id, task—blandWhat runs the call: one of these or agent_id is required
line"twilio"elevenlabs_agentstwilio or sip_trunk

See External Frameworks → Phone-call platforms.


Programmatic Agent Fields (agent_type: "programmatic")

These fields are used when agent_type is "programmatic".

FieldTypeDefaultAccepted ValuesDescription
integrationstringnullzapier, composio, n8n, mcp, pythonIntegration 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_configdict{}integration-specific key-valuesSame 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.

FieldTypeDefaultDescription
connectionstringnullThe Outbound webhook record: address, header and query rows, inbound token
outbound_timeoutint30Timeout for outbound calls (seconds)
inbound_endpointstringnullEndpoint path for inbound responses (e.g. "/webhook/human_contact"). If not set, derived from entry points.
response_mappingdict{}Field mapping for webhook response transformation
max_retriesint3Max retry attempts for failed outbound webhooks
retry_delayint5Delay between retries (seconds)
operatorsdict{}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

KeyProvider
slackSlack Bot API
telegramTelegram Bot API
discordDiscord Bot API (coming)
whatsappWhatsApp Business API (Meta Cloud API) (coming)
teamsMicrosoft Teams Bot Framework
emailSMTP 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.

FieldTypeDefaultDescription
connectionstringnullThe channel's record in the vault
bot_tokenrecord—Bot/API token for the provider (see per-provider notes below) — set on the record
signing_secretrecord—Request verification secret (see per-provider notes below) — set on the record
listen_channelslist[]Channel/chat IDs to accept inbound messages from (empty = all)
post_channelstringnullDefault channel/chat ID for outbound messages (see per-provider notes below)
verify_tokenrecord—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

Providerbot_tokensigning_secretpost_channelverify_token
slackBot OAuth token (xoxb-…)Slack signing secret (HMAC-SHA256)Channel ID (e.g. C123456)—
telegramBot token from @BotFatherSecret token set when registering webhookChat ID—
discordBot token (without Bot prefix)App public key (Ed25519 — requires pynacl)Channel ID—
whatsappMeta Graph API access tokenMeta app secret (HMAC-SHA256)Phone number IDhub.verify_token for webhook registration
teamsBot Framework App IDBot Framework App passwordConversation ID—

Inbound Route Registered Per Provider

ProviderRoute
slackPOST /channels/slack/{agent_name}/events
telegramPOST /channels/telegram/{agent_name}/webhook
discordPOST /channels/discord/{agent_name}/interactions
whatsappGET /channels/whatsapp/{agent_name}/webhook (verification) + POST (messages)
teamsPOST /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 activity

Email 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.

FieldTypeDefaultRequiredDescription
enabledbooltruenoSet false to keep the block but disable the adapter
providerstring"mailgun"noInbound webhook provider: mailgun, sendgrid, postmark
regionstring"us"noProvider region: us, eu (Mailgun: api.mailgun.net vs api.eu.mailgun.net)
connectionstringnullyesThe email record (Infrastructure → Workspace → Email); holds the signing secret and SMTP host, port, username and password
signing_secretrecord—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_domainstringnullnoDomain hosting inbound mail — used for plus-addressing (agent+THREAD@inbound.example.com)
inbound_local_part_filterlist[]noOnly process emails sent to these local parts (e.g. ["support", "help"]); empty = accept all
auto_reply_dropbooltruenoDrop RFC 3834 auto-replies (Auto-Submitted, Precedence: bulk/list, noreply senders)
smtp_hostrecord—yes (when enabled)Outbound SMTP hostname
smtp_portrecord587noSMTP port (587 STARTTLS, 465 implicit TLS)
smtp_usernamerecord—noSMTP auth username
smtp_passwordrecord—noSMTP auth password
from_addressstringnullyes (when enabled)Default From: address
reply_to_addressstringnullnoOverride Reply-To: header (defaults to from_address with plus-addressing token)
use_starttlsbooltruenoNegotiate STARTTLS for SMTP submission
subject_prefixstringnullnoPrefix for subjects, e.g. "[#TICKET-{thread_id}]" — used as fallback threading token when reference headers are stripped
threadingstring"references_first"noThread mapping strategy: references_first (prefer In-Reply-To/References headers, fall back to plus-addressing) or reply_to_token (skip headers, plus-addressing only)
attachmentsdictsee belownoInbound attachment handling — see [EmailAttachmentConfig](#emailattachmentconfig)

EmailAttachmentConfig (under channels.email.attachments):

FieldTypeDefaultDescription
receiveboolfalseOpt-in: surface inbound attachments to the agent. Off by default for security.
max_size_mbint10Max accepted attachment size in MB (1–100)
storage_urlrecord—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:

ProviderVerificationNotes
mailgunHMAC-SHA256 over timestamp + tokensigning_secret = Mailgun's HTTP webhook signing key
sendgridECDSA P-256signing_secret = base64 ECDSA verification key (Inbound Parse settings)
postmarkHTTP Basic Auth + IP allowlistsigning_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 record

See 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 defaults

Advanced Mode (Dict)

FieldTypeDefaultAccepted ValuesDescription
strategystring"recency"recency, relevance, hybridMemory retrieval strategy
limitint101 – 100Max feed posts per invocation
cross_sessionboolfalsetrue, falsePersist memory across sessions
cross_session_limitint501 – 500Max cross-session posts to retain
relevance_weightfloat0.60.0 – 1.0Weight for relevance scoring
recency_weightfloat0.40.0 – 1.0Weight for recency scoring
decay_hoursint241 – unlimitedHours 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: 24

EscalationConfig

Used inside manager.escalation.

FieldTypeDefaultDescription
targetslist of [EscalationTarget](#escalationtarget)[]Escalation targets — all fire in parallel
auto_escalatedictsee belowAuto-escalation rules

auto_escalate Fields

FieldTypeDefaultDescription
max_retriesint3Max retry attempts before escalating
max_errors_per_sessionint5Error count threshold per session
timeout_thresholdint2Consecutive 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>}].

FieldTypeDefaultAccepted ValuesApplies To
typestringrequiredhuman_agent, webhook, channelall
agentstringnullagent namehuman_agent
entry_pointstringnullentry point namehuman_agent
connectionstringnullan Outbound webhook record (Infrastructure page → Add custom): address, header and query rowswebhook
methodstring"POST"POST, PUT, PATCHwebhook
payload_templatedictnullJSON with {{var}} placeholderswebhook
providerstringnullslack, telegram, discord, whatsapp, teamschannel
channel_idstringnullchannel IDchannel
message_templatestringnulltext with {{var}} placeholderschannel

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

BrokerConfig

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

From 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")

FieldTypeDefaultRequiredDescription
typestring"kafka"yesDiscriminator
connectionstring—yesThe Apache Kafka record
bootstrap_serversrecord—yesKafka broker addresses, e.g. kafka-1:9092
security_protocolstringunsetnoPLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL
sasl_mechanismstringunsetnoPLAIN, SCRAM-SHA-256, SCRAM-SHA-512, GSSAPI
sasl_usernamerecord—noSASL username
sasl_passwordrecord—noSASL password
sasl_kerberos_service_name, oauth_scope, aws_region, aws_role_arnstringunsetnoStay in YAML (not secrets). Like security_protocol and sasl_mechanism, set them only when the broker needs them.
oauth_* credentials, PEM contentsrecord—noSet on the record
ssl_cafilestringnullnoPath to CA bundle for TLS
ssl_certfilestringnullnoClient cert (mTLS)
ssl_keyfilestringnullnoClient key (mTLS)

SQSBrokerConfig (type: "sqs")

FieldTypeDefaultRequiredDescription
typestring"sqs"yesDiscriminator
connectionstring—yesThe AWS SQS record
regionstring—yesAWS region, e.g. "us-east-1"
aws_access_key_idrecord—noOptional explicit key — otherwise IAM role credentials are used
aws_secret_access_keyrecord—noOptional explicit secret
aws_session_tokenrecord—noSTS session token (for assumed roles)
endpoint_urlrecord—noOverride endpoint (LocalStack, VPC endpoints)
role_arn, role_session_name, external_idstring—noAssumed-role settings; stay in YAML

MQTTBrokerConfig (type: "mqtt")

FieldTypeDefaultRequiredDescription
typestring"mqtt"yesDiscriminator
connectionstring—yesThe MQTT record
hostrecord—yesMQTT broker hostname
portrecord1883noPort (1883 plain, 8883 TLS)
usernamerecord—noMQTT username
passwordrecord—noMQTT password (and any client PEMs)
use_tlsboolfalsenoEnable TLS
client_idstringnullnoClient ID (random if unset)
keepalive_sint60noKeepalive 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.

FieldTypeDefaultRequiredDescription
typestring"redis_streams"yesDiscriminator
connectionstring—yesThe Redis Streams record
urlrecord—yesredis://, rediss://, or unix:// URL
dbint0noRedis 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.

FieldTypeDefaultRequiredDescription
typestring"imap"yesDiscriminator
connectionstring—yesThe IMAP Mailbox record
hostrecord—yesIMAP server hostname
portrecord993noPort (143 plain, 993 TLS)
usernamerecord—yesIMAP username (usually full email address)
passwordrecord—yesIMAP password or app-specific token
use_tlsbooltruenoUse 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
FieldTypeDefaultRequiredDescription
brokerstring—yesName of an entry in the top-level brokers: block
topicstringnullyes (Kafka)Kafka topic to subscribe to
group_idstringnullnoKafka consumer group ID (defaults to <sdk-name>-<agent-name>)
queuestringnullyes (SQS)SQS queue name. A queue URL goes on the broker record instead
streamstringnullyes (Redis Streams)Redis stream key
consumer_groupstringnullnoRedis Streams consumer group
mqtt_topicstringnullyes (MQTT)MQTT topic filter (supports + and # wildcards)
qosint1noMQTT QoS level: 0 at-most-once, 1 at-least-once, 2 exactly-once
folderstring"INBOX"noIMAP folder to monitor
poll_interval_sfloatnullnoPolling interval for IMAP / non-streaming sources
filterdictnullnoCloudEvents-style attribute equality. AND-semantics — all key/value pairs must match for the listener to fire.
deserializestringnullnoPydantic class for payload validation, format "module.path:ClassName". ValidationError routes the message to DLQ.
deliveryobjectsee [EventDeliverySpec](#eventdeliveryspec)noRetry + DLQ behavior
batch_sizeint1noMessages fetched per poll cycle (1–1000)
visibility_heartbeatboolfalseno(SQS only) Auto-extend the visibility timeout while a long-running agent handler executes

EventDeliverySpec

FieldTypeDefaultDescription
max_attemptsint5Max delivery attempts before routing to DLQ (1–50)
backoffstring"exponential"linear or exponential
backoff_initial_sfloat1.0Initial backoff delay in seconds
backoff_max_sfloat60.0Cap on backoff delay
dead_letterobjectnullDLQ 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.

FieldTypeDefaultRequiredDescription
brokerstring—yesName of a broker in the top-level brokers: block
topicstringnullwhen broker is KafkaKafka DLQ topic name
queuestringnullwhen broker is SQSSQS DLQ queue name (a queue URL goes on the broker record)
streamstringnullwhen broker is Redis StreamsRedis Streams DLQ stream key

How Listeners Fire Agents

When a message arrives:

  1. Verify — the listener verifies the source-specific signature/credentials at the connection layer.
  2. Filter — filter: keys are matched against the normalized envelope. Non-match = ack and skip (no retry).
  3. Idempotency — the runtime tracks (listener_name, message_id) to drop duplicate deliveries. If already processed, ack and skip.
  4. Deserialize — if deserialize: is set, payload is validated through the named Pydantic class. ValidationError → DLQ (no retry — bad data won't fix itself).
  5. Wrap — message is wrapped in a CloudEvents-shaped TriggerEvent envelope.
  6. Dispatch — published as an ENTRY_POINT event for the agent. Same path as a POST /webhook/{entry_point}. Agent counts toward invocation metering like any other invocation.
  7. Ack — on success, the source is acked (Kafka offset commit, SQS DeleteMessage, Redis XACK, etc.).
  8. Retry / DLQ — on failure, the message is retried per delivery.backoff until max_attempts, then routed to dead_letter if configured.

See Event Listeners — Architecture for an end-to-end walkthrough.


Top-Level Config (LeafMeshConfig)

FieldTypeDefaultAccepted ValuesDescription
namestring"default_mesh"any stringMesh name
versionstring"1.0.0"any stringConfiguration version
architecturestring"managed_mesh"managed_meshArchitecture type (only one supported)
debugboolfalsetrue, falseEnable debug mode
log_levelstring"INFO"DEBUG, INFO, WARNING, ERRORLogging level
environmentstring"development"development, productionEnvironment
managerobjectsee [ManagerConfig](#managerconfig)—Manager coordination + analysis
meshobjectsee [MeshConfig](#meshconfig--cloud-providers)—Mesh network + cloud providers
timeouts, scheduler, runtime, limits, connectors, security, self_healingobjectsee [Runtime Behaviour Blocks](configuration#runtime-behaviour-blocks)—Runtime behaviour settings
agentsdict{}agent name → AgentConfigAgent configurations
entry_pointslist[] (auto-populated with a default entry when none is provided)see [Entry Points](#entry-points)Named portals into mesh
brokersdict{}broker name → [BrokerConfig](#brokerconfig)Top-level connection definitions referenced by agent.listen_events
data_structuresdict{}name → DataStructureCustom data type definitions
auto_discoverdictnull{"directory": "path", "pattern": "*.py", "recursive": true}Auto-discover agent files
evolutionobjectsee [EvolutionConfig](#evolutionconfig)—Evolutionary optimization (enabled defaults to false)
evolution_apiobjectsee [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

FieldTypeDefaultAccepted ValuesDescription
enabledbooltruetrue, falseEnable manager analysis
modelstringnone — required when enabledsame 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.
domainstring"generic"generic, ecommerce, data_analysisManager domain specialization
promptstringnullany 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_intervenebooltruetrue, falseAllow manager interventions (false = read-only)
coordination_rulesdict{}arbitrary key-valuesUser-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_rulesdictsee below—Limits on human-input requests. Directly under manager:
timeout_rulesdictsee below—Timeout escalation. Directly under manager:
workflow_pause_rulesdictsee below—Paused-workflow limits. Directly under manager:
human_response_rulesdictsee below—What happens after each kind of human reply. Directly under manager:
chain_completion_timeoutfloat1800.0secondsWait time before checking chain completeness
health_check_intervalint60secondsSeconds between health checks
agent_timeout_thresholdint180secondsSeconds before agent is timed out
escalationobjectnullsee [EscalationConfig](#escalationconfig)Escalation targets and rules
routingdictsee 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 time

routing Fields

FieldTypeDefaultAccepted ValuesDescription
modestring"static"static, learningRouting mode (static=YAML only, learning=adaptive)
memory_sizeint1001 – 1000Max routing decisions to remember
confidence_thresholdfloat0.70.0 – 1.0Min confidence to accept learned route
fallbackstring"all"allFallback when confidence too low
decay_daysint301 – 365Days 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)

FieldTypeDefault
max_concurrent_requestsint3
max_agent_requestsint5
enable_request_queuingbooltrue

timeout_rules (Defaults)

FieldTypeDefault
max_timeouts_before_escalationint2
timeout_escalation_enabledbooltrue
escalation_notify_managersbooltrue

workflow_pause_rules (Defaults)

FieldTypeDefault
max_pause_duration_minutesint30
max_concurrent_paused_workflowsint2
pause_monitoring_enabledbooltrue

human_response_rules (Defaults)

Scenariorequires_manager_reviewauto_escalate
approvalfalsefalse
escalationtruetrue
timeouttruefalse

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

FieldTypeDefaultDescription
reply_streamboolfalseRoute mesh replies through the per-session reply stream (opt-in; always on in multi-worker mode)
bedrockobjectnullAWS Bedrock config
vertexobjectnullGoogle Vertex AI config
foundryobjectnullMicrosoft Foundry/Azure AI config
localobjectnullLocal 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)

FieldTypeDefaultDescription
connectionstringnullThe "AWS Bedrock" record under Infrastructure → Models
regionrecord"us-east-1"AWS region
profilestringnullAWS profile from ~/.aws/credentials
endpoint_urlrecord—Custom Bedrock endpoint URL

Auth: AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY env vars, IAM role, or ~/.aws/credentials

VertexConfig (Google Cloud)

FieldTypeDefaultRequiredDescription
connectionstringnullyesThe "Google Vertex AI" record under Infrastructure → Models
projectrecord—yesGCP project ID
locationrecord"us-central1"noGCP region

Auth: GOOGLE_APPLICATION_CREDENTIALS env var or gcloud auth

FoundryConfig (Azure)

FieldTypeDefaultRequiredDescription
connectionstring—yesThe "Azure AI Foundry" record under Infrastructure → Models
endpointrecord—yesFoundry endpoint URL (e.g. https://<resource>.openai.azure.com)
api_versionstringnullnoAzure API version (e.g. 2024-10-21). Omit for v1 endpoint.

Auth: AZURE_FOUNDRY_API_KEY or AZURE_FOUNDRY_TOKEN env vars

LocalModelConfig

FieldTypeDefaultRequiredDescription
connectionstringnullnoThe record under Infrastructure → Models → Local / self-hosted
endpointrecord"http://localhost:11434/api/generate"noModel server URL
server_typestringnull (auto-detect)noServer type: openai, ollama, textgen, huggingface
timeoutfloat120.0noRequest timeout in seconds for the local model (local models can be slow on CPU)
headersrecord{}noCustom 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:

Serverserver_typeExample endpoint
vLLMopenaihttp://localhost:8000/v1/chat/completions
SGLangopenaihttp://localhost:30000/v1/chat/completions
llama.cpp serveropenaihttp://localhost:8080/v1/chat/completions
LM Studioopenaihttp://localhost:1234/v1/chat/completions
LocalAIopenaihttp://localhost:8080/v1/chat/completions
TGI (--api openai)openaihttp://localhost:8080/v1/chat/completions
TensorRT-LLMopenaihttp://localhost:8000/v1/chat/completions
Triton + vLLMopenaihttp://localhost:8000/v1/chat/completions
Ollamaollamahttp://localhost:11434/api/generate
Text Gen Web UItextgenhttp://localhost:5000/api/v1/generate
HF Inferencehuggingfacehttps://api-inference.huggingface.co/models/...

Tip: Most production servers (vLLM, SGLang, TGI, llama.cpp) expose OpenAI-compatible endpoints. Use server_type: openai for all of them. If server_type is 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 # seconds

Store (.env, not YAML)

From 2.4.482 the store is set only in .env. There is no persistence: or redis: block in the YAML.

VariableDefault when unsetWhat it sets
LEAFMESH_STOREredisredis or postgres
Redis
REDIS_URLnoneThe 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_HOSTlocalhostServer host
REDIS_PORT6379Server port
REDIS_PASSWORDnoneServer password
REDIS_DB0Database number
REDIS_DEFAULT_TTL3600 (1 hour)Lifetime of general data (yields, mesh communications), seconds
REDIS_SESSION_TTL604800 (7 days)Lifetime of sessions and conversation history, seconds
REDIS_SSLfalseSet true to require TLS
REDIS_SSL_CERT_REQSrequiredCertificate check: required, optional or none
REDIS_SSL_CA_CERTSnonePath to the CA bundle
REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILEnoneClient certificate and key (mutual TLS)
REDIS_SSL_CHECK_HOSTNAMEtrueCheck the server hostname against its certificate
REDIS_CLUSTER_NODESnoneComma-separated host:port list. Setting it turns on Redis Cluster mode.
Postgres
DATABASE_URLnoneThe 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_HOSTlocalhostServer host
POSTGRES_PORT5432Server port
POSTGRES_DBleafmeshDatabase name
POSTGRES_USER, POSTGRES_PASSWORDnoneCredentials
POSTGRES_SSLMODEunset (the driver's own default)Postgres SSL mode
POSTGRES_DEFAULT_TTL3600 (1 hour)Lifetime of general data, seconds
POSTGRES_SESSION_TTL604800 (7 days)Lifetime of sessions, seconds
POSTGRES_POOL_MIN1Smallest connection pool size
POSTGRES_POOL_MAX10Largest 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.

FieldTypeDefaultDescription
cors_originslist[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
FieldDefaultDescription
evolution.enabledfalseTurn evolution on
evolution_api.enabledtrueRun the evolution/health API
evolution_api.host127.0.0.1Address the evolution/health API listens on
evolution_api.port19820Port 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

FieldTypeDefaultDescription
typestringrequiredobject, string, number, boolean, list
propertiesdictnullObject properties (for type=object)
requiredlist[]Required field names
validation_rulesdict{}Validation rules (key-value strings)

Entry Points

Each entry point is a named portal into the mesh.

FieldTypeDefaultDescription
namestringrequiredEntry point name
targetstringrequiredTarget agent name
descriptionstringoptionalDescription
conditionstring"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:

RuleConstraint
agent_typeMust be llm, human, programmatic, or external
communication_typeMust be dual, chain, or execute
optimization_strategyMust be performance, cost, or speed (or null)
max_tool_calls_per_messageMust be 0–20
tool_call_timeoutMust be > 0 and <= 300 seconds
tool_choiceMust be a non-empty string (auto, none, or tool name)
frameworkMust 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 requiredWhen agent_type is external, framework must be set
integrationMust be zapier, composio, n8n, mcp, python (2.4.492+) (or null)
integration restrictedOnly valid when agent_type is programmatic
is_human_powered syncAuto-set to true when agent_type="human", forced to false when agent_type="llm"
AgentConfig extrasAllows arbitrary extra fields (extra="allow")
LeafMeshConfig extrasRejects 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.

Fieldllmhumanprogrammaticexternal
nameRRRR
descriptionUUUU
modelU———
model_connectionU———
promptU———
temperatureU———
max_tokensU———
max_completion_tokensU———
reasoningU———
thinkingU———
sandboxU———
framework_guidanceU———
image_optionsU———
super_agentU———
reasoning_budgetU———
enable_prompt_cachingU———
optimization_strategyU———
context_partsU———
toolsU———
tool_choiceU———
max_tool_calls_per_messageU———
tool_call_timeoutU———
allow_parallel_tool_callsU———
tool_categoriesU———
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——UU
request_form——UU
price_per_1k_tokens——UU
integration——U—
communication_typeUUUU
notifyUUUU
scalingUUUU
wake_upUUUU
yieldsUUUU
inputsUUUU
can_callUUUU
narrationUUUU
knowledgeU———
wait_forUUUU
wait_for_timeoutUUUU
auto_store_responseUUUU
auto_store_yieldsUUUU
memoryU———

Legend: R = required, U = used, — = ignored, auto = auto-set

Last updated: 11/10/2026