External Frameworks

External Framework Integrations

LeafMesh orchestrates external AI frameworks as agents in your mesh. Any CrewAI crew, LangGraph graph, AutoGen team, A2A agent, MCP tool server, Zapier action, Composio integration, or n8n workflow can participate in can_call chains and wait_for patterns alongside your native LeafMesh agents.

External agents use agent_type: "external" in YAML. LeafMesh handles connector lifecycle, request/response serialization, polling, and error propagation automatically. Your Python function receives the external framework's response and post-processes it into structured yields.

How External Agents Work

Every external agent follows the same pattern:

  1. YAML declares the agent with agent_type: "external" and a framework key
  2. connector_config passes framework-specific settings (endpoints, keys, tool names)
  3. Python function post-processes the external response into your mesh's yield schema
  4. can_call / wait_for wires the external agent into your mesh like any other agent
mesh_call("entry_point", data)
    --> native agent (can_call) --> external agent --> native agent
                                      |
                                      v
                              CrewAI / LangGraph / AutoGen / A2A
                              MCP / Zapier / Composio / n8n

Data Flow

All connectors use a standard request/response envelope:

  • In: Your agent's input_data dict is sent to the external framework
  • Out: The framework's response arrives as external_response in your Python function
  • Yields: Your function returns a dict matching the agent's yields schema
# Every external agent function has this signature async def my_external_agent(external_response, input_data, context): # external_response = raw output from the framework # input_data = what the calling agent passed # context = session context, upstream yields, etc. return {"my_yield": external_response.get("result")}

1. CrewAI

Delegates tasks to a deployed CrewAI Enterprise crew. The connector hands off the request and returns the crew's final result.

Install

pip install httpx

Credentials

Make a record under Infrastructure → External agents → CrewAI. It holds the crew's endpoint and API key; the agent names it with connection:. Addresses and keys are entered only there: written inline in connector_config, including an ${{ENV}} reference, they are refused at load.

YAML Configuration

agents: crewai_research: name: "crewai_research" agent_type: "external" framework: "crewai" description: "Delegates research tasks to a CrewAI crew" connector_config: connection: "crewai-research" # Infrastructure → External agents → CrewAI: endpoint + API key poll_interval: 2.0 # seconds between status checks max_poll_seconds: 300 # timeout for crew completion yields: research_result: "string" sources: "list" inputs: topic: "string" can_call: - agent: "advisor_agent"

Python Implementation

Create agency/crewai_research_agent.py:

async def crewai_research_agent(external_response, input_data, context): """Post-process CrewAI crew results into mesh yields.""" return { "research_result": external_response.get("output", ""), "sources": external_response.get("sources", []), }

Data Flow

DirectionContent
Sent to CrewAIYour agent's input_data dict, passed as crew inputs
ReturnedThe crew's final result, delivered as external_response to your Python function

2. LangGraph

Runs a deployed LangGraph graph via the LangGraph Platform. Supports two execution paths: the official langgraph-sdk (preferred) or a built-in HTTP fallback.

Install

# Option A: Official SDK (preferred) pip install langgraph-sdk # Option B: HTTP fallback pip install httpx

Credentials

Make a record under Infrastructure → External agents → LangGraph. It holds the deployment URL and API key; the agent names it with connection:. Addresses and keys are entered only there: written inline in connector_config, including an ${{ENV}} reference, they are refused at load.

YAML Configuration

agents: langgraph_workflow: name: "langgraph_workflow" agent_type: "external" framework: "langgraph" description: "Runs a LangGraph stateful workflow" connector_config: connection: "langgraph-main" # Infrastructure → External agents → LangGraph: URL + API key graph_id: "my_graph" # graph name on the platform poll_interval: 1.0 max_poll_seconds: 300 yields: workflow_result: "object" inputs: query: "string" can_call: - agent: "processor_agent"

Python Implementation

Create agency/langgraph_workflow_agent.py:

async def langgraph_workflow_agent(external_response, input_data, context): """Post-process LangGraph graph output.""" return {"workflow_result": external_response}

Data Flow

DirectionContent
Sent to LangGraphYour agent's input_data dict, passed as the graph's input
ReturnedThe graph run's result, delivered as external_response to your Python function

3. AutoGen

Runs an AutoGen multi-agent conversation in-process. The connector creates a ConversableAgent pair and executes the conversation locally using your configured model.

Install

pip install pyautogen

YAML Configuration

agents: autogen_team: name: "autogen_team" agent_type: "external" framework: "autogen" description: "Multi-agent conversation via AutoGen" connector_config: model: "gpt-4o" # model for the AutoGen agents max_rounds: 10 # max conversation turns yields: team_result: "string" inputs: message: "string" can_call: - agent: "advisor_agent"

Python Implementation

Create agency/autogen_team_agent.py:

async def autogen_team_agent(external_response, input_data, context): """Post-process AutoGen conversation result.""" return {"team_result": external_response}

Data Flow

DirectionContent
Sent to AutoGeninput_data["message"] as the conversation starter
ReturnedThe final conversation result, delivered as external_response

4. A2A (Agent-to-Agent Protocol)

Communicates with any A2A-compliant agent server, including automatic agent discovery and support for long-running tasks. Works with the official a2a-sdk or the built-in HTTP fallback.

A2A 0.3 and A2A 1.0 are both supported. You don't choose: the connector reads the version from the agent's card, or from how the server answers, and speaks that dialect. One mesh can call 0.3 and 1.0 agents side by side.

Install

# Option A: Official SDK (preferred) pip install a2a-sdk # Option B: HTTP fallback pip install httpx

Credentials

Make a record under Infrastructure → External agents → A2A Protocol. It holds the agent's URL and, for authenticated agents, its token; the agent names it with connection:. Addresses and keys are entered only there: written inline in connector_config, including an ${{ENV}} reference, they are refused at load.

YAML Configuration

agents: a2a_partner: name: "a2a_partner" agent_type: "external" framework: "a2a" description: "Communicates with an A2A-compatible agent" connector_config: connection: "partner-a2a" # Infrastructure → External agents → A2A Protocol: URL + token auth_scheme: "Bearer" # default: Bearer poll_interval: 2.0 max_poll_seconds: 300 yields: partner_response: "object" inputs: message: "string" can_call: - agent: "processor_agent"

Python Implementation

Create agency/a2a_partner_agent.py:

async def a2a_partner_agent(external_response, input_data, context): """Post-process A2A agent response.""" return {"partner_response": external_response}

Data Flow

DirectionContent
Sent to A2A agentinput_data["message"] as the message to the remote agent; your request_form goes in the message's metadata
ReturnedThe agent's task result, delivered as external_response, plus the task's history as steps (see *What an External Agent Reports Back* below)

A conversation continues across calls: the A2A context id from one call is sent with the next in the same session. If a connector's own time limit runs out, the remote task is cancelled too, not left running.


5. MCP (Model Context Protocol)

Calls a single tool on an MCP server. Supports two transport modes: stdio (launches a local subprocess) and http (connects to a remote server). The MCP session is kept alive across calls for performance.

Works with both the 1.x and 2.x releases of the mcp package. A tool that answers with an error is a failed call on both, and a stdio server process is always stopped when the mesh stops. Your request_form fields are added to the tool's arguments — a tool with a strict input schema refuses fields it does not know, and that refusal is a failed call.

Install

# Full MCP support (stdio + http) pip install mcp # HTTP-only fallback pip install httpx

YAML Configuration (HTTP Transport)

agents: mcp_tools: name: "mcp_tools" agent_type: "external" framework: "mcp" description: "Access tools from a remote MCP server" connector_config: transport: "http" tool_name: "get_weather" connection: "weather-mcp" # Infrastructure → Integrations → MCP: server URL + token yields: tool_results: "object" inputs: city: "string"

YAML Configuration (stdio Transport)

agents: mcp_local: name: "mcp_local" agent_type: "external" framework: "mcp" description: "Local MCP tool server via stdio" connector_config: transport: "stdio" command: "npx" args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] tool_name: "read_file" yields: tool_results: "object" inputs: path: "string"

Python Implementation

Create agency/mcp_tools_agent.py:

async def mcp_tools_agent(external_response, input_data, context): """Post-process MCP tool call result.""" return {"tool_results": external_response}

Integration Helpers

You can wire MCP tools into an existing agent's pipeline without creating a separate external agent. The mcp() helper works with @pre_compose, @chain, and @conditional_chain.

From 2.4.497 the helper names a vault record instead of an address. Make the record on the Tools page; it holds the server's address and sign-in. Code that still passes an address (mcp("https://…", "tool")) or an inline auth_token is refused, with a message naming the record to create.

from leafmesh import pre_compose, chain, conditional_chain, mcp # Before the model call: add an MCP tool's result to the context @pre_compose(context_processor=mcp("get_weather", connection="weather_mcp")) async def weather_agent(llm_response, input_data, context): return {"forecast": llm_response} # After the agent: store the result through an MCP tool @chain(mcp("store_result", connection="results_mcp")) async def analysis_agent(llm_response, input_data, context): return {"analysis": llm_response.get("summary")} # Only when the agent asks for it @conditional_chain( lambda r, ctx: r.get("needs_enrichment"), mcp("enrich_data", connection="enrich_mcp"), ) async def enricher(llm_response, input_data, context): return {"needs_enrichment": True, "data": llm_response}

A local server that you launch by command keeps its command in code. Its keys come from the vault: each row of the record becomes an environment variable of that process.

@chain(mcp("lookup", connection="local_tools", transport="stdio", command="npx", args=["-y", "@acme/mcp-server"])) async def looker(llm_response, input_data, context): return llm_response

Data Flow

DirectionContent
Sent to MCP serverYour input_data dict, passed as tool arguments
ReturnedThe tool's result content, delivered as external_response

6. Zapier

Triggers Zapier actions to connect with 8,000+ apps, through Zapier MCP. Zapier retired AI Actions, the REST API the old api_key reached; a Zapier agent configured with only an api_key now fails with that explanation. Use an MCP key (mcp_key), saved on the Zapier record.

Install

pip install httpx

Credentials

Make a record under Infrastructure → Integrations → Zapier. It holds the Zapier MCP key; name it with vault_connection:. Keys are entered only there: written inline in connector_config, including an ${ENV} reference, they are refused at load.

YAML Configuration

agents: zapier_gmail: name: "zapier_gmail" agent_type: "external" framework: "zapier" description: "Send emails via Zapier Gmail integration" connector_config: connection: "gmail" # the Zapier app (tool = gmail_send_email) action: "send_email" vault_connection: "zapier-mcp" # Infrastructure → Integrations → Zapier: holds mcp_key timeout: 60.0 yields: action_result: "object" inputs: to: "string" subject: "string" body: "string"

Python Implementation

Create agency/zapier_gmail_agent.py:

async def zapier_gmail_agent(external_response, input_data, context): """Post-process Zapier action result.""" return {"action_result": external_response}

Integration Helpers

Wire Zapier actions into any agent's pipeline. The zapier() helper works with @pre_compose, @chain, and @conditional_chain. From 2.4.485 every helper takes a vault record: zapier("<action name from your Zapier MCP server>", connection="<Zapier record>"). The action string is an action your own Zapier MCP server offers (for example gmail_send_email); the record, made under Infrastructure → Integrations → Zapier, holds the Zapier MCP key. A key or address passed in code is refused.

from leafmesh import conditional_chain, zapier # Action names come from your own Zapier MCP server; gmail_send_email is an example. # Conditionally send email after agent @conditional_chain( lambda r, ctx: r.get("send_email"), zapier("gmail_send_email", connection="zapier-main"), ) async def email_handler(llm_response, input_data, context): return {"send_email": True, "to": "user@example.com", "body": "..."}

Data Flow

DirectionContent
Sent to ZapierYour input_data dict, passed as action parameters
ReturnedThe action's result, delivered as external_response to your Python function

7. Composio

Executes Composio actions to access 250+ pre-built integrations (GitHub, Slack, Gmail, Jira, Salesforce, and more) with managed authentication. The connector uses the Composio Python SDK.

Install

Included with pip install leafmesh — composio-core is a mandatory dependency, no extra install needed.

Credentials

Make a record under Infrastructure → Integrations → Composio. It holds the Composio API key; the agent names it with connection:. Addresses and keys are entered only there: written inline in connector_config, including an ${{ENV}} reference, they are refused at load.

YAML Configuration

agents: composio_github: name: "composio_github" agent_type: "external" framework: "composio" description: "Star a GitHub repo via Composio" connector_config: action: "GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER" connection: "composio-main" # Infrastructure → Integrations → Composio: API key entity_id: "default" yields: integration_result: "object" inputs: owner: "string" repo: "string"

YAML Configuration (Nested Inputs)

For Composio actions with structured request bodies (e.g. Google Calendar), use nested inputs so the frontend renders individual sub-field editors:

agents: composio_calendar: name: "composio_calendar" agent_type: "external" framework: "composio" description: "Create a Google Calendar event via Composio" connector_config: action: "GOOGLECALENDAR_CREATE_EVENT" connection: "composio-main" # Infrastructure → Integrations → Composio yields: data: type: object fields: event_id: string status: string inputs: connected_account_id: string user_id: string arguments: type: object fields: summary: string start_datetime: string end_datetime: string timezone: string

The nested arguments.fields definition tells the frontend to render summary, start_datetime, end_datetime, and timezone as individual input fields inside the arguments object, rather than a single text box.

Python Implementation

Create agency/composio_github_agent.py:

async def composio_github_agent(external_response, input_data, context): """Post-process Composio action result.""" return {"integration_result": external_response}

Integration Helpers

Wire Composio into any agent's pipeline. The composio() helper works with @pre_compose, @chain, and @conditional_chain. It takes the Composio record (Infrastructure → Integrations → Composio): composio("ACTION", connection="<record>"). A key passed in code is refused.

from leafmesh import pre_compose, chain, conditional_chain, composio # Before LLM call — fetch GitHub repo data @pre_compose( context_processor=composio( "GITHUB_GET_A_REPOSITORY", connection="composio-main", params={"owner": "leafcraftstudios", "repo": "leafmesh"}, ) ) async def repo_agent(llm_response, input_data, context): return {"result": llm_response} # After agent — post to Slack via Composio @chain(composio("SLACK_SEND_MESSAGE", connection="composio-main", params={"channel": "#alerts"})) async def alert_agent(llm_response, input_data, context): return {"text": llm_response.get("alert")} # Conditionally create Jira ticket after agent @conditional_chain( lambda r, ctx: r.get("needs_ticket"), composio("JIRA_CREATE_ISSUE", connection="composio-main"), ) async def support_agent(llm_response, input_data, context): return {"needs_ticket": True, "summary": "Bug in login flow"}

Data Flow

DirectionContent
Sent to ComposioYour input_data dict, passed as action parameters
ReturnedThe action's execution result, delivered as external_response

8. n8n

Triggers n8n workflows via webhook URLs. n8n exposes workflows as webhooks -- this connector hands off your data to the webhook and returns the workflow's response.

Install

pip install httpx

Credentials

Make a record under Infrastructure → Integrations → n8n. It holds the workflow's webhook URL and, optionally, its auth token and the instance's API key; the agent names it with connection:. Addresses and keys are entered only there: written inline in connector_config, including an ${{ENV}} reference, they are refused at load.

YAML Configuration

agents: n8n_enrich: name: "n8n_enrich" agent_type: "external" framework: "n8n" description: "Trigger an n8n workflow for data enrichment" connector_config: connection: "n8n-enrich" # Infrastructure → Integrations → n8n: webhook URL + token timeout: 60.0 yields: workflow_result: "object" inputs: payload: "object" can_call: - agent: "processor_agent"

Python Implementation

Create agency/n8n_enrich_agent.py:

async def n8n_enrich_agent(external_response, input_data, context): """Post-process n8n workflow response.""" return {"workflow_result": external_response}

Integration Helpers

Wire n8n into any agent's pipeline. The n8n() helper works with @pre_compose, @chain, and @conditional_chain. It takes the n8n record that holds the workflow's webhook URL and token (Infrastructure → Integrations → n8n): n8n(connection="<record>"). A webhook URL or token passed in code is refused (2.4.485).

from leafmesh import pre_compose, chain, conditional_chain, n8n # Before LLM call — enrich context with n8n workflow @pre_compose( context_processor=n8n(connection="n8n-enrich") ) async def enrichment_agent(llm_response, input_data, context): return {"result": llm_response} # After agent — trigger n8n notification workflow @chain(n8n(connection="n8n-notify")) async def alert_agent(llm_response, input_data, context): return {"event": "completed", "data": llm_response} # Conditionally trigger n8n workflow @conditional_chain( lambda r, ctx: r.get("trigger_workflow"), n8n(connection="n8n-process"), ) async def processor(llm_response, input_data, context): return {"trigger_workflow": True, "payload": llm_response}

Data Flow

DirectionContent
Sent to n8nYour input_data dict, sent to the configured webhook URL, with your request_form fields in the same body and the mesh session id as _leafmesh_session_id
ReturnedThe n8n workflow's response, delivered as external_response

The workflow reads everything as $json.body. _leafmesh_session_id lets a workflow keep its own memory per conversation (for example a Window Buffer Memory keyed on it).

Seeing each node's result. n8n's webhook reply is only the last node's output. Add the instance's n8n API key as the record's api_key field (scopes: execution list and read) and the connector looks up this call's own execution and reports every node as a step (see What an External Agent Reports Back below) — including the node that failed and why. Each call carries its own request id, so the lookup never picks up another run. Without the key, the call works the same, with no steps.

connector_config: connection: "n8n-prod" # the record holds webhook_url and, for per-node steps, api_key

Mesh Patterns with External Agents

External agents participate in all standard mesh patterns. Here are common configurations.

can_call Chains

Route from a native agent to an external agent, then back to a native agent:

agents: triage_agent: name: "triage_agent" agent_type: "llm" can_call: - agent: "crewai_research" condition: "calling_agent_response.needs_deep_research == true" - agent: "processor_agent" condition: "calling_agent_response.needs_deep_research == false" crewai_research: name: "crewai_research" agent_type: "external" framework: "crewai" connector_config: connection: "crewai-research" # Infrastructure → External agents → CrewAI: endpoint + API key can_call: - agent: "advisor_agent"

wait_for Fan-In

Use an external agent as one branch of a fan-in pattern:

agents: processor_agent: name: "processor_agent" agent_type: "programmatic" can_call: - agent: "langgraph_workflow" - agent: "native_researcher" advisor_agent: name: "advisor_agent" agent_type: "llm" wait_for: "processor_agent AND (langgraph_workflow OR native_researcher)" wait_for_timeout: 120

The advisor activates as soon as processor_agent completes AND whichever research path (LangGraph or native) finishes first.

Mixed External Agents

Combine multiple external frameworks in one mesh:

agents: intake_agent: name: "intake_agent" agent_type: "llm" can_call: - agent: "mcp_tools" condition: "calling_agent_response.needs_tools == true" - agent: "a2a_partner" condition: "calling_agent_response.needs_partner == true" - agent: "composio_github" condition: "calling_agent_response.needs_github == true" final_agent: name: "final_agent" agent_type: "llm" wait_for: "intake_agent AND (mcp_tools OR a2a_partner OR composio_github)"

Governing External Agents (external_policy)

An external agent is someone else's system: the mesh sends it data and puts its answer in front of the next agent. One optional external_policy: block on the agent says what may go out, what may come back, and when to stop calling it. An agent with no block at all is already safe — every default is the strict one.

The same rules govern a programmatic agent that calls a connector through integration: — it is held to the same hosts, fields, reply size and quarantine as an external agent.

agents: n8n_enrich: name: "n8n_enrich" agent_type: "external" framework: "n8n" connector_config: connection: "n8n-enrich" # the record holds the webhook URL external_policy: allowed_hosts: ["api.example.com", "*.example.com"] max_response_bytes: 1000000 # default 1 MB quarantine_after: 5 # default 5 failed calls in a row send_fields: ["company", "domain"] redact: ["\\b\\d{16}\\b"] # extra patterns masked in what is sent yields: employees: "number"
FieldDefaultMeaning
allowed_hosts[]Hosts the agent may call besides the host of its own configured address (url, endpoint, webhook_url, base_url, or the vendor default). Exact names, or *.example.com for any subdomain. Any other host is refused before the request is sent.
max_response_bytes1000000Largest reply accepted. A larger reply is refused as a failure — never cut short.
quarantine_after5Failed calls in a row after which the agent is quarantined. A success resets the count.
send_fieldsall fieldsOnly these top-level input fields are sent. When it is set, nothing else goes — the conversation history, chain history and rerun note too go only when you list them.
redact[]Extra regular expressions replaced with [redacted] in everything sent.

Always on, whatever the block says:

  • Non-local hosts must use HTTPS, and redirects are never followed. A private or link-local address is refused unless it is the agent's own configured host, so a local n8n in development keeps working.
  • Vault secrets and internal _ fields are never sent (without send_fields, the conversation history, chain history and rerun note still are).
  • A policy that does not validate refuses the call; it is never replaced by looser defaults.
  • Every answer is marked as coming from outside, and the next agent's prompt shows it as quoted data, not instructions.
  • Each call's record — hashes of what was sent and received, outcome, duration, cost — is attached to that call's trace.

Quarantine and release

After quarantine_after failed calls in a row, the agent is quarantined: further calls fail at once without reaching it, and the Inbox is told. An A2A agent whose Agent Card changes is quarantined too, until a person re-approves it. The approved card is kept per agent, address and tenant.

A call that was stopped because its session was cancelled is neither a success nor a failure: it never moves the count toward quarantine.

EndpointPurpose
POST /api/agents/{name}/quarantineStop an external agent now. Optional body: reason, by.
POST /api/agents/{name}/releaseRelease it: clears the failure count; a changed A2A Agent Card becomes the approved one.

Approval is routing

There is no per-action approval inside the policy. The old skills risk levels (send, spend, delete) and unknown_skill_risk are refused at load when set to one of those. To have a person approve before an external agent acts, route the work to a human agent with a can_call condition, and give that human agent the can_call to the external agent.

Budgets

Every external agent can have a monthly_token_budget or monthly_cost_budget. It is checked before every call, and the agent stops when it is spent. Where the platform reports what a call used, that is counted; where it does not, the mesh estimates each call from what it sends and receives. Set price_per_1k_tokens to give an estimated agent a cost, so a cost budget applies to it. A call that would put the agent ahead of the month's even pace is held back, and budget_on_exhaust decides whether it stops or escalates. Calls racing for the last of a budget cannot overspend it, and a stop for budget is never passed on as an answer — the next agent does not run on it. See Per-Agent Budgets.

Sending Your Own Fields (request_form)

Many systems take more than the message: an n8n workflow reads a customer_tier, a LangGraph graph reads a region from its configuration, an A2A agent reads metadata. request_form is a set of keys and values you design, sent with every request to the external agent, placed where that system expects caller data.

agents: enrich: name: "enrich" agent_type: "external" framework: "n8n" connector_config: connection: "n8n-prod" request_form: source: "leafmesh" region: "eu" limits: { max_rows: 50 }

Where each system receives it:

frameworkWhere the form goes
n8nThe webhook body ($json.body)
a2a and the A2A platforms (servicenow, sap_joule, workday, box, atlassian_rovo, gemini_enterprise)The message's metadata (A2A 0.3 and 1.0)
langgraphconfig.configurable
dify, wordware, crewaiinputs
flowiseoverrideConfig.vars
lyzrsystem_prompt_variables
velluminputs (one entry per key)
agentforcevariables (one entry per key)
stackai, bedrock_agentcoreThe request body
claude_managed, foundry_agents, mistral_agents, glean_agentsmetadata
vertex_agent_engineinput
copilot_studioThe message activity's value
mcp, zapier, composioThe tool's arguments

Refused at load — these have no place for caller data on the path LeafMesh uses: writer, watsonx, autogen, hermes, relevance, claude_agent, openai_agents, openai_agents_api, letta, and custom (a custom agent's own function reads connector_config instead).

The rules:

  • Any keys and any JSON values (text, numbers, true/false, null, lists, maps).
  • At most 50 keys and 16 KB in all. A key is 1–64 letters, digits, _, - or ., and may not start with _ (that prefix is the SDK's own).
  • No secrets. A secret written in the form is refused at load. Credentials the system needs belong on the agent's record, named with connection:.
  • The form fills keys, it never replaces one: if the SDK already sets a key there (the message, a session or conversation id), the SDK's value is sent and the load warns you.
  • Values are never logged; only key names appear in the trace.
  • It works on external agents and on programmatic agents with an integration:. Any other agent type is refused.
  • A change saved from Studio or the API applies from the next call, with no restart.

Secrets in connector_config

From 2.4.481 keys, tokens, passwords, client ids, addresses (URLs, endpoints, hosts) and header or query rows are entered only in the vault: make the record under Infrastructure → External agents → the framework (Zapier, n8n, Composio and MCP under Infrastructure → Integrations) and name it with connection: (Zapier: vault_connection:). That covers url, base_url, endpoint, webhook_url, headers, api_key, token, secret, client_id / client_secret, tenant_id, aws_*, service_account_json and the like. Written inline, including an ${ENV:default} reference, they are refused at load, and the error names the field and the record to create. The connection name itself may come from the environment: connection: "${PARTNER_CONNECTION:partner-a2a}" is fine.

connector_config: connection: "partner-a2a" # holds url and token

If the record does not exist yet, the mesh still boots: the agent is switched off with the reason and starts working as soon as the record exists, with no restart.

A record with no address (2.4.490+). An external agent or integration whose record has no address is switched off at start, and checked again on every vault save. It is never called and never quarantined, and it switches back on by itself once the address is added. Studio shows the reason.

The record sets the kind (2.4.491+). An external agent set up on Studio's External agents page (or with POST /api/external-agents) takes its framework from the vault record it picks. A custom record must be Custom: A2A (a2a) or Custom: API (custom_api). A record that does not fit is refused there with one of:

  • the vault record '<name>' is a <Label> record — this agent is <Label>, not <Other>
  • the vault record '<name>' is a custom record — choose Custom: A2A or Custom: API for it (a2a or custom_api)

In YAML, an agent picks a set-up external agent by name and writes no framework or connector_config beside it:

agents: partner_quote: name: "partner_quote" external_agent: "partner-quoting" # set up on the External agents page

Writing framework or connector_config next to external_agent is refused at load: framework is set on the External agents page for '<name>' — remove it from agent '<agent>' (it picks external_agent: <name>). The older inline style, with framework and connector_config in the agent and no external_agent, still loads as before and is not checked against a record.

EndpointPurpose
GET /api/external-agents/records2.4.491+. With no framework, every record an external agent may use: {name, status, framework, framework_label, custom} (framework is null for a custom record).
POST /api/external-agents/from-spec2.4.491+. Body {"spec": "<Swagger/OpenAPI text>"}. Lists the POST, PUT and PATCH operations (method, path) a Custom API agent can call, and the spec's servers. It saves nothing: the address still goes only in the vault.

The record is resolved on every call, so a credential rotated on the Infra page is used from the next call without a restart, and a tenant's own record of the same name is used for that tenant's runs. See Bring Your Own Vault. The only place a single vault value is written as a reference is the env of an mcp stdio process: ${vault:<field>}.

When you read an agent back (GET /api/agents, GET /api/yaml/agents/{name}), secrets, webhook paths and Authorization values are masked, so you can see which connection a field uses without seeing its value.

Conversations Carry On

When the external system has its own idea of a conversation, a follow-up in the same LeafMesh session continues the same conversation on the far side. The handle the system gives back is kept for that agent, session and tenant, for as long as the session lives, and sent with the next call:

frameworkWhat continues
a2aThe context id
agentforceThe Agentforce session
claude_agentThe Agent SDK session (resumed)
openai_agentsThe previous response
difyThe Dify conversation
langgraphThe thread
watsonxThe thread
relevanceThe conversation
hermesThe session
stackaiThe user id
n8nThe mesh session id, sent as _leafmesh_session_id
claude_managedThe Managed Agents session
openai_agents_apiThe agent session
foundry_agentsThe Foundry conversation
vertex_agent_engineThe Agent Engine session
bedrock_agentcoreThe runtime session
copilot_studioThe Direct Line conversation
mistral_agentsThe Mistral conversation
lettaThe Letta conversation

Two sessions or two tenants never share one. A handle is kept only from a call that succeeded, and if the far side has forgotten it, it is dropped and a fresh conversation starts.

What an External Agent Reports Back

Besides its answer, each connector reports the steps its system ran — the tool calls, nodes, tasks or messages that system records — as a short list: each step's name, kind, whether it worked and, if not, a short reason. Labels and reasons only, never the data; at most 30 steps, with the rest counted.

Examples: an n8n workflow's nodes (with api_key set), the history of an A2A task, a CrewAI crew's tasks, a LangGraph thread's history, Dify and Vellum node events, AutoGen run messages, Hermes function calls, Writer's graph lookups. Zapier, MCP and Composio report their one action.

Where the steps show up:

  • The trace of the run, as child steps of the external call.
  • The feed, as one line: steps: look up customer ✓, send email ✗ (mailbox full).
  • Lessons. The agent's experience is judged from the run's record — what happened and why, retries, the fields sent and returned, whether the reply matched last time, the failure streak, quarantine — and a successful run that took a new path of steps can become a lesson.
  • The board ticket, as its checklist.

The record never holds addresses, values or contact details, and is never handed to the next agent or to another external system. Cost and time stay in the trace; they are not part of the record the feed and lessons read. When the system reports what a call cost, it is counted in the run's budget; when it reports nothing, the trace says the price is unknown.

A run that stopped early — truncated, timed out, paused or cancelled — says so on its record.

Stopping a Call

When a session is cancelled (for example, stopped from the Control page) while an external agent is still working, the call stops: the request in flight is abandoned, polling stops, and the connector asks the system to cancel its job where the system allows it. The stopped call never counts toward quarantine.

Changing an External Agent While the Mesh Runs

These changes apply to a running mesh, with no restart:

  • framework, connection and any connector_config setting — the agent's connector is rebuilt.
  • A credential saved, rotated or deleted on the Infra page — every agent using that connection picks it up.
  • can_call — routing follows the new list on every route.
  • external_policy and request_form — read on every call.

Through the API, PATCH /api/yaml/agents/{name} merges one level deep: sending {"external_policy": {"quarantine_after": 3}} changes that one field and keeps send_fields and allowed_hosts. Inside such a block, a key sent as null is removed. PUT /api/yaml/agents/{name} replaces the agent whole.

On claude_agent, cwd, allowed_tools and permission_mode give the agent rights on the host machine, so the API never changes them. From 2.4.485 you change them on the host with the CLI, and the change is saved as a new version of the agency's configuration:

leafmesh agent host-rights <agent> [--cwd DIR] [--allowed-tools a,b] [--permission-mode M] [--clear FIELD] [--yes]

--cwd sets the working directory, --allowed-tools the comma-separated tools it may use, and --permission-mode its permission mode: exactly default, acceptEdits, plan or bypassPermissions (anything else is refused by the command). --clear takes cwd, allowed_tools or permission_mode and can be repeated. --yes applies the change without asking.


Supported Frameworks

The connectors documented in detail above are the most common ones. LeafMesh ships connectors for 42 frameworks and platforms in total, plus custom. Each is selected with the matching framework key on an agent_type: "external" agent. Its connector_config fields are discoverable at runtime via GET /api/registry?category=connectors — query it to see the exact fields each connector accepts in your deployment.

Multi-agent frameworks

frameworkPurposeIntegration Mode
crewaiRun a CrewAI crew as an agentRemote service
langgraphRun a LangGraph graph as an agentRemote service (SDK or HTTP fallback)
autogenRun an AutoGen team as an agentRemote service
a2aCall any Agent-to-Agent protocol agentRemote service (SDK or HTTP fallback)
custom_apiCall an agent of your own behind an HTTP API (a Custom: API record, 2.4.486)Remote service

Agent platforms reached over A2A

Each of these is the A2A connector under the platform's own name, so it has its own entry in Studio's list, its own card and connections on the Infra page, and its own name in traces and cost. Its connector_config is the same as a2a: the record's url is the agent's A2A address (its agent card URL).

frameworkPlatform
servicenowServiceNow AI Agents
sap_jouleSAP Joule agents
workdayWorkday agents
boxBox AI agents
atlassian_rovoAtlassian Rovo agents
gemini_enterpriseGemini Enterprise agents

In-process agent SDKs

frameworkPurposeIntegration Mode
claude_agentRun the Claude Agent SDK in-processIn-process library
openai_agentsRun the OpenAI Agents SDK in-processIn-process library

Tooling / automation

frameworkPurposeIntegration Mode
mcpCall a Model Context Protocol tool serverLocal (stdio) or remote server
zapierTrigger a Zapier actionRemote service / MCP
composioRun a Composio integrationManaged SDK
n8nTrigger an n8n workflowWebhook

Hosted agent / chatbot platforms

frameworkPurposeIntegration Mode
lyzrCall a Lyzr agentHTTP service
writerCall a Writer (Palmyra) model/agentHTTP service
difyCall a Dify appHTTP service
flowiseCall a Flowise chatflowHTTP service
vellumCall a Vellum workflow deploymentHTTP service
stackaiCall a StackAI flowHTTP service
agentforceCall a Salesforce Agentforce agentHTTP service
watsonxCall an IBM watsonx agentHTTP service
relevanceCall a Relevance AI agentHTTP service
wordwareCall a Wordware appHTTP service
hermesCall a Hermes agent endpointHTTP service
mistral_agentsCall a Mistral agent (from Mistral AI Studio)HTTP service
lettaCall a Letta agent (Letta Cloud or your own server)HTTP service
glean_agentsCall a Glean agent (from Glean Agent Builder)HTTP service
lindyStart a Lindy agent through its Webhook Received triggerHTTP service (callback)

Managed agent platforms

The agent is built and runs on the vendor's platform. The mesh sends it the input and takes the finished result, keeping one conversation per mesh session and tenant. A run that passes its time limit, or whose mesh session ends, is stopped on the platform where it allows that.

frameworkPlatformRequired connector_config
claude_managedClaude Managed Agentsagent_id, environment_id, api_key
openai_agents_apiOpenAI Agents APIagent_id, api_key
foundry_agentsMicrosoft Foundry Agentsagent_name, and endpoint or resource
copilot_studioMicrosoft Copilot Studio (Dynamics 365)token_endpoint or direct_line_secret
vertex_agent_engineGoogle Vertex AI Agent Engineproject, location, engine_id
bedrock_agentcoreAWS Bedrock AgentCoreagent_runtime_arn

The required fields for the other hosted agents: mistral_agents needs agent_id and api_key; letta needs agent_id (and api_key for Letta Cloud; base_url for your own server); glean_agents needs agent_id, api_token, and instance or base_url.

Keys, secrets and addresses in these lists (api_key, api_token, endpoint, token_endpoint, direct_line_secret, base_url, webhook_url, webhook_secret) are fields of the agent's record under Infrastructure → External agents, named with connection:; never write them in connector_config.

Lindy answers by calling back, so it uses after_handoff: wait (its default from 2.4.508; move_on also works). Store the trigger's URL and its generated secret on a connection (webhook_url, webhook_secret) and name it with connection:. In your Lindy agent, end the flow with Send POST Request to Callback, sending back _leafmesh_session_id and _leafmesh_request_id from the request with your result. With after_handoff: reply the call only starts the agent.

agents: lindy_agent: agent_type: external framework: lindy connector_config: connection: lindy_main # webhook_url + webhook_secret from the vault after_handoff: wait # 2.4.508 (was mode: callback)

Phone-call platforms

2.4.508. An agent can hand a phone call to ElevenLabs Agents, Vapi, Retell or Bland. The platform makes the call and runs the whole conversation; the mesh never touches the audio. When the call is over, the platform's own "call finished" webhook brings the outcome back, and the session moves on through the agent's routes.

frameworkPlatformRequired connector_config
elevenlabs_agentsElevenLabs Agents (over Twilio or a SIP trunk: line: twilio or sip_trunk)agent_id, phone_number_id
vapiVapiassistant_id, phone_number_id
retellRetell AIfrom_number (agent_id optional)
blandBland AIone of pathway_id, persona_id, agent_id or task (from_number optional)

How to set one up:

  1. Make the platform's record under Infrastructure → External agents (its API key, its webhook signing secret and account ids such as phone_number_id) and name it with connection:.
  2. Set the platform's webhook address to <mesh>/callback/calls/<framework>, for example https://mesh.example.com/callback/calls/vapi. Bland is sent the address with each call. The route is signed, so the mesh needs LEAFMESH_WEBHOOK_SECRET.
  3. Give the agent a phone_number input (E.164, e.g. +14155550100). Its other text, number and yes/no inputs become the call's variables; a request_form adds fixed ones.
agents: outreach_call: agent_type: external framework: vapi connector_config: connection: vapi-main # Infrastructure → External agents → Vapi assistant_id: "<your Vapi assistant id>" inputs: {phone_number: string}

after_handoff is wait (default) or move_on; reply is refused, because a platform never answers in the reply. callback_timeout defaults to 1800 seconds, since a call rings, talks and is analysed before its webhook comes.

The agent's answer has the same shape on every platform: call_id, status (completed, no_answer, busy, voicemail or failed), ended_reason, summary, transcript, data, successful and duration_seconds. A call that did not happen (no answer, busy, voicemail, failed to connect) is a failed answer, with the reason in error.

All connectors are included with pip install leafmesh. Composio is a mandatory dependency; framework-specific SDKs (CrewAI, LangGraph, the Claude/OpenAI agent SDKs, etc.) are optional extras (e.g. pip install leafmesh[crewai]); the HTTP/platform connectors need no extra packages.


LeafMesh -- Multi-agent orchestration through managed mesh

Last updated: 11/10/2026