Skip to content

A2A Agent Orchestration

%%{init: {'look': 'handDrawn', 'theme': 'base', 'themeVariables': {'primaryColor': '#eef2ff', 'primaryBorderColor': '#1e40af', 'primaryTextColor': '#1e293b', 'lineColor': '#1e3a8a', 'edgeLabelBackground': '#ffffff', 'clusterBkg': '#fbfcff', 'clusterBorder': '#2563eb', 'fontFamily': '-apple-system, system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif', 'fontSize': '15px'}, 'flowchart': {'nodeSpacing': 50, 'rankSpacing': 58, 'padding': 14, 'htmlLabels': true, 'curve': 'basis'}}}%%
flowchart LR
  P(["Proposal"]) --> R

  subgraph remote["someone else's agents · asked in parallel"]
    direction TB
    R("Risk specialist")
    C("Cost specialist")
  end

  P --> C
  R --> S("One combined<br/>recommendation")
  C --> S
  style remote stroke-dasharray: 6 5

Outcome: a workflow where deterministic tasks own the control flow and remote A2A agents do the specialist reasoning — verified reachable before delegation, called in parallel with idempotency keys, joined, then synthesized.

The shape

The agents here are independently operated: separately deployed, separately versioned, reachable only over the A2A protocol. The workflow does not know how they reason and does not try to. What it owns is everything around them — whether they are reachable, how long they get, how many run at once, what happens when one fails, and how their outputs combine.

That division is the point. Each AGENT branch is an independent durable task: if Conductor restarts mid-flight, both in-flight delegations resume rather than restarting. If the cost agent fails and the risk agent succeeds, the JOIN surfaces that asymmetry instead of discarding the good result.

GET_AGENT_CARD runs first as a pre-flight check. Delegating to an endpoint that is down produces a timeout several minutes later; discovering it up front produces an immediate, legible failure.

It is marked optional: true on purpose. Without that flag the task fails terminally on an unreachable agent and takes the workflow with it, which means the reachability SWITCH below it never runs — the branch reads like a safety net but is dead code. With optional: true the task lands in COMPLETED_WITH_ERRORS, execution continues, and the SWITCH terminates with a remote_agent_unreachable output you can act on.

A locally runnable setup

You do not need external endpoints to try this. Any Conductor workflow can be served as an A2A agent — set metadata: {"a2a.enabled": true} on its definition and it is exposed at {basePath}/{workflowName}. The served workflow receives the caller's text as ${workflow.input._a2a_text}.

The A2A server is opt-in and off by default. Enable it on your server:

conductor.a2a.server.enabled=true

The default conductor.a2a.server.basePath is /a2a, so the two specialist workflows below are reachable at <YOUR-CLUSTER-URL>/a2a/risk_specialist_agent and <YOUR-CLUSTER-URL>/a2a/cost_specialist_agent.

Save this as a2a-risk-specialist.json:

{
  "name": "risk_specialist_agent",
  "version": 1,
  "schemaVersion": 2,
  "description": "A Conductor workflow exposed as an A2A agent. Assesses delivery risk for a proposal and returns a structured finding.",
  "ownerEmail": "cookbook@example.com",
  "metadata": {
    "a2a.enabled": true,
    "a2a.tags": [
      "risk",
      "review"
    ]
  },
  "timeoutSeconds": 300,
  "timeoutPolicy": "TIME_OUT_WF",
  "tasks": [
    {
      "name": "assess_risk",
      "taskReferenceName": "assess_risk",
      "type": "LLM_CHAT_COMPLETE",
      "inputParameters": {
        "llmProvider": "openai",
        "model": "gpt-4o-mini",
        "messages": [
          {
            "role": "system",
            "message": "You are a delivery risk reviewer. Return JSON: {\"domain\": \"risk\", \"findings\": [string], \"severity\": \"low\"|\"medium\"|\"high\"}. Recommend only; never approve or execute anything."
          },
          {
            "role": "user",
            "message": "${workflow.input._a2a_text}"
          }
        ],
        "temperature": 0.2,
        "maxTokens": 600,
        "jsonOutput": true
      }
    }
  ],
  "outputParameters": {
    "domain": "${assess_risk.output.result.domain}",
    "findings": "${assess_risk.output.result.findings}",
    "severity": "${assess_risk.output.result.severity}"
  }
}

Save this as a2a-cost-specialist.json:

{
  "name": "cost_specialist_agent",
  "version": 1,
  "schemaVersion": 2,
  "description": "A Conductor workflow exposed as an A2A agent. Assesses cost exposure for a proposal and returns a structured finding.",
  "ownerEmail": "cookbook@example.com",
  "metadata": {
    "a2a.enabled": true,
    "a2a.tags": [
      "cost",
      "review"
    ]
  },
  "timeoutSeconds": 300,
  "timeoutPolicy": "TIME_OUT_WF",
  "tasks": [
    {
      "name": "assess_cost",
      "taskReferenceName": "assess_cost",
      "type": "LLM_CHAT_COMPLETE",
      "inputParameters": {
        "llmProvider": "openai",
        "model": "gpt-4o-mini",
        "messages": [
          {
            "role": "system",
            "message": "You are a cost reviewer. Return JSON: {\"domain\": \"cost\", \"findings\": [string], \"severity\": \"low\"|\"medium\"|\"high\"}. Recommend only; never approve or execute anything."
          },
          {
            "role": "user",
            "message": "${workflow.input._a2a_text}"
          }
        ],
        "temperature": 0.2,
        "maxTokens": 600,
        "jsonOutput": true
      }
    }
  ],
  "outputParameters": {
    "domain": "${assess_cost.output.result.domain}",
    "findings": "${assess_cost.output.result.findings}",
    "severity": "${assess_cost.output.result.severity}"
  }
}

Runnable definition

Save this as a2a-orchestration.json:

{
  "name": "a2a_agent_orchestration",
  "description": "Deterministic workflow that verifies two remote A2A agents are reachable, delegates to both in parallel with caller-supplied idempotency keys, joins their findings, and synthesizes a recommendation without acting on it.",
  "version": 1,
  "schemaVersion": 2,
  "timeoutSeconds": 900,
  "timeoutPolicy": "TIME_OUT_WF",
  "inputParameters": [
    "proposal",
    "riskAgentUrl",
    "costAgentUrl",
    "requestId"
  ],
  "tasks": [
    {
      "name": "discover_risk_agent",
      "taskReferenceName": "risk_card",
      "type": "GET_AGENT_CARD",
      "inputParameters": {
        "agentType": "a2a",
        "agentUrl": "${workflow.input.riskAgentUrl}"
      },
      "optional": true
    },
    {
      "name": "verify_agents_reachable",
      "taskReferenceName": "verify_agents",
      "type": "JSON_JQ_TRANSFORM",
      "inputParameters": {
        "card": "${risk_card.output}",
        "queryExpression": "{reachable: (((.card // {}) | tojson | contains(\"\\\"name\\\"\")) == true), advertised: (((.card.name // .card.agentCard.name) // \"unknown\"))}"
      }
    },
    {
      "name": "route_on_reachability",
      "taskReferenceName": "route_reachable",
      "type": "SWITCH",
      "evaluatorType": "value-param",
      "expression": "reachable",
      "inputParameters": {
        "reachable": "${verify_agents.output.result.reachable}"
      },
      "decisionCases": {
        "true": [
          {
            "name": "fork_specialist_agents",
            "taskReferenceName": "fork_specialists",
            "type": "FORK_JOIN",
            "forkTasks": [
              [
                {
                  "name": "delegate_risk_review",
                  "taskReferenceName": "risk_review",
                  "type": "AGENT",
                  "inputParameters": {
                    "agentType": "a2a",
                    "agentUrl": "${workflow.input.riskAgentUrl}",
                    "text": "Assess delivery risk for this proposal: ${workflow.input.proposal}",
                    "idempotencyKey": "${workflow.input.requestId}-risk",
                    "pollIntervalSeconds": 5
                  }
                }
              ],
              [
                {
                  "name": "delegate_cost_review",
                  "taskReferenceName": "cost_review",
                  "type": "AGENT",
                  "inputParameters": {
                    "agentType": "a2a",
                    "agentUrl": "${workflow.input.costAgentUrl}",
                    "text": "Assess cost exposure for this proposal: ${workflow.input.proposal}",
                    "idempotencyKey": "${workflow.input.requestId}-cost",
                    "pollIntervalSeconds": 5
                  }
                }
              ]
            ]
          },
          {
            "name": "join_specialist_agents",
            "taskReferenceName": "join_specialists",
            "type": "JOIN",
            "joinOn": [
              "risk_review",
              "cost_review"
            ]
          },
          {
            "name": "synthesize_recommendation",
            "taskReferenceName": "synthesize",
            "type": "LLM_CHAT_COMPLETE",
            "inputParameters": {
              "llmProvider": "openai",
              "model": "gpt-4o-mini",
              "messages": [
                {
                  "role": "system",
                  "message": "Combine independent specialist findings into one recommendation. Return JSON: {\"recommendation\": string, \"blockers\": [string], \"highestSeverity\": \"low\"|\"medium\"|\"high\"}. Attribute each blocker to the specialist domain it came from. Do not invent findings neither specialist reported."
                },
                {
                  "role": "user",
                  "message": "Proposal: ${workflow.input.proposal}\nSpecialist findings: ${join_specialists.output}"
                }
              ],
              "temperature": 0.1,
              "maxTokens": 800,
              "jsonOutput": true
            }
          }
        ],
        "false": [
          {
            "name": "terminate_agent_unreachable",
            "taskReferenceName": "terminate_unreachable",
            "type": "TERMINATE",
            "inputParameters": {
              "terminationStatus": "FAILED",
              "workflowOutput": {
                "error": "remote_agent_unreachable",
                "agentUrl": "${workflow.input.riskAgentUrl}"
              }
            }
          }
        ]
      },
      "defaultCase": []
    }
  ],
  "outputParameters": {
    "recommendation": "${synthesize.output.result.recommendation}",
    "blockers": "${synthesize.output.result.blockers}",
    "highestSeverity": "${synthesize.output.result.highestSeverity}",
    "riskAgent": "${risk_review.output}",
    "costAgent": "${cost_review.output}"
  }
}

Register and run

Register the two specialists over REST, not with the CLI

conductor workflow create drops the metadata block. The definition registers fine, but metadata comes back {}, the workflow is never exposed as an A2A agent, and every /a2a/... path returns 404. Use the metadata API for any workflow that relies on metadata:

curl -X POST '<YOUR-CLUSTER-URL>/api/metadata/workflow?overwrite=true' \
  -H 'Content-Type: application/json' -d @a2a-risk-specialist.json
curl -X POST '<YOUR-CLUSTER-URL>/api/metadata/workflow?overwrite=true' \
  -H 'Content-Type: application/json' -d @a2a-cost-specialist.json

Confirm each agent is actually exposed before orchestrating — this is also the fastest way to catch the metadata problem above:

curl -s <YOUR-CLUSTER-URL>/a2a/risk_specialist_agent/.well-known/agent-card.json

A live agent returns a card with protocolVersion, preferredTransport: JSONRPC, and a skills entry whose tags are the a2a.tags from the definition. A 404 means the metadata did not persist.

The orchestrator has no metadata, so the CLI is fine for it:

conductor workflow create a2a-orchestration.json
conductor workflow start -w a2a_agent_orchestration -i '{"proposal":"Migrate the billing service to a new payments provider in Q3.","riskAgentUrl":"<YOUR-CLUSTER-URL>/a2a/risk_specialist_agent","costAgentUrl":"<YOUR-CLUSTER-URL>/a2a/cost_specialist_agent","requestId":"proposal-1042"}'

Open Executions in the Conductor UI and select the new execution to review the task graph, and each task's inputs and outputs.

The two AGENT tasks should show overlapping start and end times — that is the fan-out working. Each also records the remote taskId, which is what you reconcile against if a delegation has to be retried.

Against two locally served specialists the whole run takes roughly 12–18 seconds, with each delegation about 5 seconds and the two overlapping. Point riskAgentUrl at a workflow that does not exist to see the unreachable path: the card task lands in COMPLETED_WITH_ERRORS and the workflow fails in about 3 seconds with remote_agent_unreachable.

Production notes

  • agentType picks the protocol, not the framework. Only a2a and conductor exist; there's no vendor-specific type.
  • Idempotency keys must survive a retry. They come from the caller, and each branch derives its own from that.
  • A remote agent is someone else's code. Validate what it returns; a prompt is not a schema.
  • Register anything using metadata over REST. The CLI drops the block, and the agent silently never gets exposed.
  • Bound each delegation on its own so a slow agent can't eat the other's budget.
  • Synthesis is advice. Route consequential actions through HITL approval.