Opening and reading tasks
Open a task (SendMessage)
With an approved binding, the caller opens a task by naming the callee.
Every route that takes a callee (.../tasks, .../tasks/{id},
.../tasks/{id}/cancel) accepts either its name or its id — pick whichever
your code already has on hand. By name is the agent's name field
(for example invoice-classifier); By id is its id field, a permanent UUID
(hover either tab for the short version):
import os
from forgebench import Forgebench, PermissionDeniedError
client = Forgebench(api_key=os.environ["CALLER_AGENT_KEY"],
base_url="https://api.forgebench.ai")
try:
task = client.agents.call(
"invoice-classifier",
text="Classify this invoice",
data={"invoice_id": "INV-2026-0912"},
wait=30, # block up to 30s for an answer
)
except PermissionDeniedError:
# no binding yet, still pending the callee's approval, or revoked —
# see /docs/a2a#1-grant-a-binding and /docs/a2a/cards#errors-specific-to-a2a
log.error("not permitted to call invoice-classifier")
raise
if task.state == "completed":
print(task.artifact_text()) # every artifact's text, joined
elif task.state == "input_required":
print(task.question) # the callee is asking something back
elif task.state == "failed":
print(task.error)
else:
# still "working" after 30s — poll it
task = client.agents.task("invoice-classifier", task.id, wait=20)import { Forgebench, PermissionDeniedError, artifactText, messageText } from "@seedlinglabs/forgebench-sdk";
const forgebench = new Forgebench({
baseUrl: "https://api.forgebench.ai",
apiKey: process.env.CALLER_AGENT_KEY!,
});
let task;
try {
task = await forgebench.agents.call("invoice-classifier", {
text: "Classify this invoice",
data: { invoice_id: "INV-2026-0912" },
wait: 30,
});
} catch (e) {
if (e instanceof PermissionDeniedError) {
// no binding yet, still pending the callee's approval, or revoked —
// see /docs/a2a#1-grant-a-binding and /docs/a2a/cards#errors-specific-to-a2a
console.error("not permitted to call invoice-classifier");
}
throw e;
}
if (task.status.state === "completed") {
console.log(artifactText(task));
} else if (task.status.state === "input_required") {
console.log(messageText(task.status.message));
} else if (task.status.state === "failed") {
console.log(task.error);
} else {
const polled = await forgebench.agents.getTask("invoice-classifier", task.id, { wait: 20 });
}import ai.forgebench.CallParams;
import ai.forgebench.Forgebench;
import ai.forgebench.errors.PermissionDeniedException;
import ai.forgebench.types.Task;
import java.time.Duration;
import java.util.Map;
Forgebench client = Forgebench.builder()
.apiKey(System.getenv("CALLER_AGENT_KEY"))
.baseUrl("https://api.forgebench.ai")
.build();
Task task;
try {
task = client.agents().call("invoice-classifier", CallParams.text("Classify this invoice")
.data(Map.of("invoice_id", "INV-2026-0912"))
.wait(Duration.ofSeconds(30)) // block up to 30s for an answer
.build());
} catch (PermissionDeniedException e) {
// no binding yet, still pending the callee's approval, or revoked —
// see /docs/a2a#1-grant-a-binding and /docs/a2a/cards#errors-specific-to-a2a
log.error("not permitted to call invoice-classifier");
throw e;
}
if (task.getState().equals("completed")) {
System.out.println(task.artifactText()); // every artifact's text, joined
} else if (task.getState().equals("input_required")) {
System.out.println(task.getQuestion()); // the callee is asking something back
} else if (task.getState().equals("failed")) {
System.out.println(task.getError());
} else {
// still "working" after 30s — poll it
task = client.agents().task("invoice-classifier", task.getId(), Duration.ofSeconds(20));
}curl -X POST "https://api.forgebench.ai/v1/agents/invoice-classifier/tasks?wait=30" \
-H "Authorization: Bearer $CALLER_AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": {
"role": "user",
"parts": [
{"text": "Classify this invoice"},
{"data": {"invoice_id": "INV-2026-0912"}}
]
}
}'import os
from forgebench import Forgebench, PermissionDeniedError
client = Forgebench(api_key=os.environ["CALLER_AGENT_KEY"],
base_url="https://api.forgebench.ai")
CALLEE_ID = "706608c0-4160-452a-86ac-ded1186326ac"
try:
task = client.agents.call(
CALLEE_ID,
text="Classify this invoice",
data={"invoice_id": "INV-2026-0912"},
wait=30, # block up to 30s for an answer
)
except PermissionDeniedError:
# no binding yet, still pending the callee's approval, or revoked —
# see /docs/a2a#1-grant-a-binding and /docs/a2a/cards#errors-specific-to-a2a
log.error("not permitted to call %s", CALLEE_ID)
raise
if task.state == "completed":
print(task.artifact_text()) # every artifact's text, joined
elif task.state == "input_required":
print(task.question) # the callee is asking something back
elif task.state == "failed":
print(task.error)
else:
# still "working" after 30s — poll it
task = client.agents.task(CALLEE_ID, task.id, wait=20)import { Forgebench, PermissionDeniedError, artifactText, messageText } from "@seedlinglabs/forgebench-sdk";
const forgebench = new Forgebench({
baseUrl: "https://api.forgebench.ai",
apiKey: process.env.CALLER_AGENT_KEY!,
});
const CALLEE_ID = "706608c0-4160-452a-86ac-ded1186326ac";
let task;
try {
task = await forgebench.agents.call(CALLEE_ID, {
text: "Classify this invoice",
data: { invoice_id: "INV-2026-0912" },
wait: 30,
});
} catch (e) {
if (e instanceof PermissionDeniedError) {
// no binding yet, still pending the callee's approval, or revoked —
// see /docs/a2a#1-grant-a-binding and /docs/a2a/cards#errors-specific-to-a2a
console.error("not permitted to call", CALLEE_ID);
}
throw e;
}
if (task.status.state === "completed") {
console.log(artifactText(task));
} else if (task.status.state === "input_required") {
console.log(messageText(task.status.message));
} else if (task.status.state === "failed") {
console.log(task.error);
} else {
const polled = await forgebench.agents.getTask(CALLEE_ID, task.id, { wait: 20 });
}import ai.forgebench.CallParams;
import ai.forgebench.Forgebench;
import ai.forgebench.errors.PermissionDeniedException;
import ai.forgebench.types.Task;
import java.time.Duration;
import java.util.Map;
Forgebench client = Forgebench.builder()
.apiKey(System.getenv("CALLER_AGENT_KEY"))
.baseUrl("https://api.forgebench.ai")
.build();
String CALLEE_ID = "706608c0-4160-452a-86ac-ded1186326ac";
Task task;
try {
task = client.agents().call(CALLEE_ID, CallParams.text("Classify this invoice")
.data(Map.of("invoice_id", "INV-2026-0912"))
.wait(Duration.ofSeconds(30)) // block up to 30s for an answer
.build());
} catch (PermissionDeniedException e) {
// no binding yet, still pending the callee's approval, or revoked —
// see /docs/a2a#1-grant-a-binding and /docs/a2a/cards#errors-specific-to-a2a
log.error("not permitted to call {}", CALLEE_ID);
throw e;
}
if (task.getState().equals("completed")) {
System.out.println(task.artifactText()); // every artifact's text, joined
} else if (task.getState().equals("input_required")) {
System.out.println(task.getQuestion()); // the callee is asking something back
} else if (task.getState().equals("failed")) {
System.out.println(task.getError());
} else {
// still "working" after 30s — poll it
task = client.agents().task(CALLEE_ID, task.getId(), Duration.ofSeconds(20));
}curl -X POST "https://api.forgebench.ai/v1/agents/706608c0-4160-452a-86ac-ded1186326ac/tasks?wait=30" \
-H "Authorization: Bearer $CALLER_AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": {
"role": "user",
"parts": [
{"text": "Classify this invoice"},
{"data": {"invoice_id": "INV-2026-0912"}}
]
}
}'That curl call, run against a real pull callee that claimed and answered its task, looks like this end to end:

A few things worth knowing before you wire this into real code:
waitblocks the request up to that many seconds (max 30) for the task to leavesubmitted/working. If it's still working whenwaitruns out, the response is the task as it currently stands, not an error — poll it again withGetTask.wait=0(or omitted) returns immediately with whatever state the task is in right after it's opened. Useful if you'd rather fire-and-poll than hold a request open.- A message part is exactly one of
text,data(structured JSON), orfile({uri, name, mime}) — never more than one on the same part. Useparts=directly when you need afilepart or more than one of a kind;text=/data=build a single-part message for you.
Continue a conversation
An input_required task means the callee is asking a question instead of
finishing. Answer it by opening a new task on the same callee with that task's context_id. That keeps a back-and-forth in one conversation:
task = client.agents.call("invoice-classifier", text="Classify this invoice", wait=30)
if task.state == "input_required":
task = client.agents.call(
"invoice-classifier",
text="It's a recurring SaaS charge, category: software",
context_id=task.context_id,
wait=30,
)let task = await forgebench.agents.call("invoice-classifier", { text: "Classify this invoice", wait: 30 });
if (task.status.state === "input_required") {
task = await forgebench.agents.call("invoice-classifier", {
text: "It's a recurring SaaS charge, category: software",
contextId: task.context_id,
wait: 30,
});
}Task task = client.agents().call("invoice-classifier",
CallParams.text("Classify this invoice").wait(Duration.ofSeconds(30)).build());
if (task.getState().equals("input_required")) {
task = client.agents().call("invoice-classifier",
CallParams.text("It's a recurring SaaS charge, category: software")
.contextId(task.getContextId())
.wait(Duration.ofSeconds(30))
.build());
}# first task; the response carries context_id (and status.state)
curl -X POST "https://api.forgebench.ai/v1/agents/invoice-classifier/tasks?wait=30" \
-H "Authorization: Bearer $CALLER_AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{"message": {"role": "user", "parts": [{"text": "Classify this invoice"}]}}'
# if status.state is "input_required", answer on the same context_id
curl -X POST "https://api.forgebench.ai/v1/agents/invoice-classifier/tasks?wait=30" \
-H "Authorization: Bearer $CALLER_AGENT_KEY" \
-H "Content-Type: application/json" \
-d @- <<JSON
{
"context_id": "$CONTEXT_ID",
"message": {"role": "user", "parts": [{"text": "It's a recurring SaaS charge, category: software"}]}
}
JSONCALLEE_ID = "706608c0-4160-452a-86ac-ded1186326ac"
task = client.agents.call(CALLEE_ID, text="Classify this invoice", wait=30)
if task.state == "input_required":
task = client.agents.call(
CALLEE_ID,
text="It's a recurring SaaS charge, category: software",
context_id=task.context_id,
wait=30,
)const CALLEE_ID = "706608c0-4160-452a-86ac-ded1186326ac";
let task = await forgebench.agents.call(CALLEE_ID, { text: "Classify this invoice", wait: 30 });
if (task.status.state === "input_required") {
task = await forgebench.agents.call(CALLEE_ID, {
text: "It's a recurring SaaS charge, category: software",
contextId: task.context_id,
wait: 30,
});
}String CALLEE_ID = "706608c0-4160-452a-86ac-ded1186326ac";
Task task = client.agents().call(CALLEE_ID,
CallParams.text("Classify this invoice").wait(Duration.ofSeconds(30)).build());
if (task.getState().equals("input_required")) {
task = client.agents().call(CALLEE_ID,
CallParams.text("It's a recurring SaaS charge, category: software")
.contextId(task.getContextId())
.wait(Duration.ofSeconds(30))
.build());
}CALLEE_ID=706608c0-4160-452a-86ac-ded1186326ac
# first task; the response carries context_id (and status.state)
curl -X POST "https://api.forgebench.ai/v1/agents/$CALLEE_ID/tasks?wait=30" \
-H "Authorization: Bearer $CALLER_AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{"message": {"role": "user", "parts": [{"text": "Classify this invoice"}]}}'
# if status.state is "input_required", answer on the same context_id
curl -X POST "https://api.forgebench.ai/v1/agents/$CALLEE_ID/tasks?wait=30" \
-H "Authorization: Bearer $CALLER_AGENT_KEY" \
-H "Content-Type: application/json" \
-d @- <<JSON
{
"context_id": "$CONTEXT_ID",
"message": {"role": "user", "parts": [{"text": "It's a recurring SaaS charge, category: software"}]}
}
JSONRead or cancel a task (GetTask / CancelTask)
task = client.agents.task("invoice-classifier", task_id, wait=10)
client.agents.cancel("invoice-classifier", task_id)const task = await forgebench.agents.getTask("invoice-classifier", taskId, { wait: 10 });
await forgebench.agents.cancelTask("invoice-classifier", taskId);Task task = client.agents().task("invoice-classifier", taskId, Duration.ofSeconds(10));
client.agents().cancel("invoice-classifier", taskId);# read (0-30s optional long-poll via ?wait=)
curl "https://api.forgebench.ai/v1/agents/invoice-classifier/tasks/$TASK_ID?wait=10" \
-H "Authorization: Bearer $CALLER_AGENT_KEY"
# cancel — only the caller may; idempotent
curl -X POST "https://api.forgebench.ai/v1/agents/invoice-classifier/tasks/$TASK_ID/cancel" \
-H "Authorization: Bearer $CALLER_AGENT_KEY"CALLEE_ID = "706608c0-4160-452a-86ac-ded1186326ac"
task = client.agents.task(CALLEE_ID, task_id, wait=10)
client.agents.cancel(CALLEE_ID, task_id)const CALLEE_ID = "706608c0-4160-452a-86ac-ded1186326ac";
const task = await forgebench.agents.getTask(CALLEE_ID, taskId, { wait: 10 });
await forgebench.agents.cancelTask(CALLEE_ID, taskId);String CALLEE_ID = "706608c0-4160-452a-86ac-ded1186326ac";
Task task = client.agents().task(CALLEE_ID, taskId, Duration.ofSeconds(10));
client.agents().cancel(CALLEE_ID, taskId);CALLEE_ID=706608c0-4160-452a-86ac-ded1186326ac
# read (0-30s optional long-poll via ?wait=)
curl "https://api.forgebench.ai/v1/agents/$CALLEE_ID/tasks/$TASK_ID?wait=10" \
-H "Authorization: Bearer $CALLER_AGENT_KEY"
# cancel — only the caller may; idempotent
curl -X POST "https://api.forgebench.ai/v1/agents/$CALLEE_ID/tasks/$TASK_ID/cancel" \
-H "Authorization: Bearer $CALLER_AGENT_KEY"Reading a task you're not the caller or callee of returns 404, not 403
— its existence isn't something an uninvolved reader should learn either
way. The caller, the callee or a console viewer role can read a task. Only the caller can cancel one, and canceling a finished task is a no-op, not an error.

