Skip to content
Ironflow is pre-1.0: APIs can change between releases. Pin a version. What is ready →

Expose an agent over A2A

The Agent2Agent (A2A) protocol lets an agent in one system send a task to an agent in a different system. ironflow a2a exposes an Ironflow agent to such callers. One A2A task is one durable Ironflow run.

  • An Ironflow server is running.
  • A worker serves an agent that you built with agent().
  • You have an API key with the developer role for the adapter.

The adapter always gives the agent this input:

{
"text": "all text parts of the message, joined with a newline",
"data": { "the first data part": "or null" },
"a2a": { "contextId": "...", "messageId": "...", "message": { "the full A2A message": "..." } }
}
import { agent } from "@ironflow/node/agent";
export const reviewAgent = agent(
{ id: "code-review", name: "Code review", description: "Reviews a pull request", triggers: [] },
async ({ event, llm, approve }) => {
const { text } = event.data as { text: string };
const findings = await llm.complete({
messages: [{ role: "user", content: `Review:\n${text}` }],
call: () => provider.complete(text),
});
const decision = await approve("code-review.post-review.approved", { ttl: "24h", payload: findings });
return { approved: decision.approved, findings };
},
);

The agent’s name and description go into the Agent Card. To expose an agent that has a different input shape, write a small agent with this input that calls the other agent with spawn().

Terminal window
set -x IRONFLOW_A2A_BEARER_TOKEN (openssl rand -hex 32)
set -x IRONFLOW_API_KEY ifkey_...
ironflow a2a --agent code-review --caller-input code-review/code-review.post-review.approved

In zsh or bash, use export NAME=value in place of set -x NAME value.

The Agent Card is now at http://127.0.0.1:9124/agents/code-review/.well-known/agent-card.json. Give the callers that address and the token.

3. Decide which pauses a caller can answer

Section titled “3. Decide which pauses a caller can answer”

An approve() call stops the run until an event with the name that you pass arrives. By default the adapter hides each such wait from remote callers: the task shows as working, and a reply is refused. This keeps a caller from answering a wait that is for your own staff.

--caller-input <function-id>/<event-name> lists one event name that a caller can send. --caller-input code-review/code-review.post-review.approved opens the wait in the agent above. The adapter splits the entry at the first /, so the event name can contain /. The adapter emits the event name with { runId, approved }, so the name must be the one that the agent passes to approve(). The task then shows as TASK_STATE_INPUT_REQUIRED, and its status message carries the approve() payload. The caller answers with a message for the same task that has a data part:

{ "approved": true, "reason": "optional text", "payload": { "optional": "JSON" } }

The adapter does not read an approval from text. A reply without a boolean approved gets an error that shows this shape.

The caller sees the approve() payload, so put only caller-safe data in it.

When a gate reaches its ttl with no reply, the agent’s SDK decides the result. The Node SDK fails the run, and the task becomes failed. The Go SDK returns a result that is not approved, and the agent continues.

The adapter serves plain HTTP and listens on loopback by default. For remote callers:

  1. Put a TLS proxy in front of the adapter.
  2. Start the adapter with --public-url https://agents.example.com, so the Agent Card shows the public address.

On a loopback listener, the adapter refuses a request whose Host header is not loopback and is not the --public-url host. This stops a web page that points its own domain at 127.0.0.1 (DNS rebinding). A proxy on the same machine can send the loopback address or the public host.

OperationResult
Send a messageStarts a run. The message ID is an idempotency key, so a retry gets the same task.
Send a streaming messageStarts a run and streams its states until it finishes or pauses.
Get or list tasksReads the runs of this agent.
Subscribe to a taskStreams the states of a run that is in progress.
Cancel a taskCancels the run.
  • All callers share one token and can see the tasks of the other callers for the same agent.
  • Callers see only the runs that the adapter started. Runs of the same function that staff, events or schedules started stay hidden.
  • A send waits 30 seconds at most. A longer task comes back as working.
  • Push notifications and model-token streaming are not available.

See the ironflow a2a reference for all flags.