Agent
Reference article: Part of the API reference.
Ask a workstream's agent a question from your own code and get its answer back. The agent answers the same way it does in the app: with the workstream's context, its connectors, and your organization's knowledge. Answers use NTUs just as they would in the app.
You can wait for an answer in two ways:
- Poll: start the answer, then retrieve it until it's finished. Simple, and fine for background jobs.
- Stream: pass
stream: trueand read the answer as it's written, through server-sent events. Best when a person is waiting.
Answers started through the API live in their own conversations, separate from people's chats with the agent. Only responses started through the API are readable through it. To post into the workstream's shared room, use Messages.
The AgentResponse object
{
"id": "5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37",
"object": "agent_response",
"workstream": "3f6c2a1e-8b4d-4e2f-9a7c-5d1b0e6f2a94",
"conversation": "conv_4Hq8Zt2Lm9Wx6Rb3Nc1K",
"status": "completed",
"output_text": "Three suppliers raised prices this quarter:\n\n- **Northwind**: +4.5%\n- **Contoso**: +2%\n- **Fabrikam**: +7%, effective 1 November",
"error": null,
"created": 1758792310,
"updated": 1758792338
}
| Field | Type | Description |
|---|---|---|
id |
string | The response's ID. |
object |
string | Always agent_response. |
workstream |
string or null | The ID of the workstream whose agent answered. |
conversation |
string | The conversation this answer belongs to. Pass it back to continue the conversation. |
status |
string | queued, in_progress, completed, failed, or cancelled. |
output_text |
string or null | The answer, in Markdown. Set once status is completed. It holds the answer only, not the agent's reasoning or tool calls. |
error |
object or null | Set when status is failed: { "message": "…" }, saying what went wrong. |
created |
integer | When the answer was started, in Unix seconds. |
updated |
integer | When it last changed, in Unix seconds. |
completed, failed, and cancelled are final: a response in one of them won't change again.
Ask the agent
Starts an answer from the workstream's agent.
POST /v1/workstreams/{workstream}/agent/responses
Scope: agent:write
| Parameter | In | Type | Description |
|---|---|---|---|
workstream |
path | string | The workstream's id or slug. The key must be able to reach it. |
The request body takes these fields:
| Body field | Type | Description |
|---|---|---|
input |
string | The question or instruction, in plain text or Markdown, 1–16,000 characters. |
conversation |
string, optional | Continue an earlier conversation: pass the conversation of a previous response. Letters, digits, _ and -, up to 64 characters. |
stream |
boolean, optional | true to stream the answer as server-sent events. Default false. |
Polling for the answer
Without stream, the request returns at once with 202 and an AgentResponse, usually with status queued. Retrieve it until the status is final:
::: code-group
curl https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/agent/responses \
-H "Authorization: Bearer $NIMBUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input": "Which suppliers raised prices this quarter?"}'
# Then, with the id from the response:
curl https://api.gonimbus.ai/v1/agent/responses/5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37 \
-H "Authorization: Bearer $NIMBUS_API_KEY"
const headers = {
Authorization: `Bearer ${process.env.NIMBUS_API_KEY}`,
'Content-Type': 'application/json',
}
const FINAL = new Set(['completed', 'failed', 'cancelled'])
const start = await fetch('https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/agent/responses', {
method: 'POST',
headers,
body: JSON.stringify({ input: 'Which suppliers raised prices this quarter?' }),
})
let answer = await start.json()
while (!FINAL.has(answer.status)) {
await new Promise((resolve) => setTimeout(resolve, 2000))
const poll = await fetch(`https://api.gonimbus.ai/v1/agent/responses/${answer.id}`, { headers })
answer = await poll.json()
}
if (answer.status === 'completed') console.log(answer.output_text)
else console.error(answer.status, answer.error?.message)
import os
import time
import requests
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['NIMBUS_API_KEY']}"
FINAL = {"completed", "failed", "cancelled"}
answer = session.post(
"https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/agent/responses",
json={"input": "Which suppliers raised prices this quarter?"},
).json()
while answer["status"] not in FINAL:
time.sleep(2)
answer = session.get(f"https://api.gonimbus.ai/v1/agent/responses/{answer['id']}").json()
if answer["status"] == "completed":
print(answer["output_text"])
else:
print(answer["status"], (answer["error"] or {}).get("message"))
:::
{
"id": "5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37",
"object": "agent_response",
"workstream": "3f6c2a1e-8b4d-4e2f-9a7c-5d1b0e6f2a94",
"conversation": "conv_4Hq8Zt2Lm9Wx6Rb3Nc1K",
"status": "queued",
"output_text": null,
"error": null,
"created": 1758792310,
"updated": 1758792310
}
Rather than polling, you can also subscribe a webhook endpoint to agent.response.completed, which carries the finished AgentResponse.
Streaming the answer
With "stream": true, the API responds with 200 and Content-Type: text/event-stream, and sends the answer as it's written. Each event has a name and a JSON data line:
event: response.created
data: {"id":"5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37","object":"agent_response","status":"queued",…}
event: response.output_text.delta
data: {"response":"5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37","delta":"Three suppliers raised "}
event: response.output_text.delta
data: {"response":"5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37","delta":"prices this quarter:"}
event: response.completed
data: {"id":"5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37","object":"agent_response","status":"completed","output_text":"Three suppliers raised prices this quarter: …",…}
| Event | data |
Meaning |
|---|---|---|
response.created |
The AgentResponse | The answer has started. Keep its id in case the stream drops. |
response.output_text.delta |
{ "response": "<id>", "delta": "…" } |
The next piece of the answer. Append deltas in order to build the Markdown. |
response.completed |
The AgentResponse | The answer is finished; output_text holds all of it. The stream ends. |
response.failed |
The AgentResponse | The agent couldn't answer; see error.message. The stream ends. |
response.cancelled |
The AgentResponse | The answer was cancelled. The stream ends. |
response.timeout |
{ "response": "<id>", "message": "…" } |
The stream was open for 10 minutes and closed. The agent keeps working: retrieve the response for its result. |
error |
{ "message": "…", "request_id": "req_…" } |
The stream itself failed. Retrieve the response for its result. |
Validation and access errors, such as an unknown workstream, come back as ordinary JSON errors before any stream starts.
::: code-group
curl -N https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/agent/responses \
-H "Authorization: Bearer $NIMBUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input": "Which suppliers raised prices this quarter?", "stream": true}'
const response = await fetch('https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/agent/responses', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NIMBUS_API_KEY}`,
'Content-Type': 'application/json',
Accept: 'text/event-stream',
},
body: JSON.stringify({ input: 'Which suppliers raised prices this quarter?', stream: true }),
})
if (!response.ok || !response.body) throw new Error(`Nimbus API ${response.status}: ${await response.text()}`)
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
let buffer = ''
let responseId: string | undefined
const handle = (event: string, data: any) => {
if (event === 'response.created') responseId = data.id
else if (event === 'response.output_text.delta') process.stdout.write(data.delta)
else if (event === 'response.completed') console.log('\n\nDone.')
else if (event === 'response.failed') console.error('\nFailed:', data.error?.message)
else if (event === 'response.timeout' || event === 'error') console.error('\nRetrieve', responseId, 'for the result.')
}
for (;;) {
const { value, done } = await reader.read()
if (done) break
buffer += value
let boundary: number
while ((boundary = buffer.indexOf('\n\n')) !== -1) {
const block = buffer.slice(0, boundary)
buffer = buffer.slice(boundary + 2)
let event = 'message'
let data = ''
for (const line of block.split('\n')) {
if (line.startsWith('event:')) event = line.slice(6).trim()
else if (line.startsWith('data:')) data += line.slice(5).trim()
}
if (data) handle(event, JSON.parse(data))
}
}
import json
import os
import requests
with requests.post(
"https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/agent/responses",
headers={
"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}",
"Accept": "text/event-stream",
},
json={"input": "Which suppliers raised prices this quarter?", "stream": True},
stream=True,
) as response:
response.raise_for_status()
event, response_id = None, None
for line in response.iter_lines(decode_unicode=True):
if line.startswith("event:"):
event = line[len("event:"):].strip()
elif line.startswith("data:"):
data = json.loads(line[len("data:"):].strip())
if event == "response.created":
response_id = data["id"]
elif event == "response.output_text.delta":
print(data["delta"], end="", flush=True)
elif event == "response.completed":
print("\n\nDone.")
elif event == "response.failed":
print("\nFailed:", data["error"]["message"])
elif event in ("response.timeout", "error"):
print("\nRetrieve", response_id, "for the result.")
:::
If the connection drops mid-answer, the agent carries on. Retrieve the response by the id from response.created to get the finished answer; don't ask again, which would start (and bill) a second answer.
Continuing a conversation {#continuing-a-conversation}
Each response has a conversation. Pass it with your next question and the agent answers with the earlier exchange in mind, so a follow-up such as "and which of those supply the EU?" makes sense. Leave it out to start a new conversation.
::: code-group
curl https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/agent/responses \
-H "Authorization: Bearer $NIMBUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input": "And which of those supply the EU?", "conversation": "conv_4Hq8Zt2Lm9Wx6Rb3Nc1K"}'
const response = await fetch('https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/agent/responses', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NIMBUS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ input: 'And which of those supply the EU?', conversation: previous.conversation }),
})
response = requests.post(
"https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/agent/responses",
headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
json={"input": "And which of those supply the EU?", "conversation": previous["conversation"]},
)
:::
Retrieve an agent response
GET /v1/agent/responses/{agent_response}
Scope: agent:read
| Parameter | In | Type | Description |
|---|---|---|---|
agent_response |
path | string | The response's id. |
::: code-group
curl https://api.gonimbus.ai/v1/agent/responses/5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37 \
-H "Authorization: Bearer $NIMBUS_API_KEY"
const response = await fetch('https://api.gonimbus.ai/v1/agent/responses/5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37', {
headers: { Authorization: `Bearer ${process.env.NIMBUS_API_KEY}` },
})
const answer = await response.json()
response = requests.get(
"https://api.gonimbus.ai/v1/agent/responses/5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37",
headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
)
answer = response.json()
:::
Returns the AgentResponse. An ID that doesn't belong to a response started through the API fails with 404 resource_missing.
Cancel an agent response
Stops an answer that is still queued or in_progress. Cancelling a response that's already final changes nothing.
POST /v1/agent/responses/{agent_response}/cancel
Scope: agent:write
| Parameter | In | Type | Description |
|---|---|---|---|
agent_response |
path | string | The response's id. |
::: code-group
curl -X POST https://api.gonimbus.ai/v1/agent/responses/5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37/cancel \
-H "Authorization: Bearer $NIMBUS_API_KEY"
const response = await fetch('https://api.gonimbus.ai/v1/agent/responses/5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37/cancel', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.NIMBUS_API_KEY}` },
})
const answer = await response.json()
response = requests.post(
"https://api.gonimbus.ai/v1/agent/responses/5e2b8c14-7a3f-4d9e-a1b6-0c4d2f8e9a37/cancel",
headers={"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}"},
)
answer = response.json()
:::
Returns the AgentResponse as it stands after the request. If its status isn't cancelled yet, retrieve it again in a moment.
FAQ
How long does an answer take?
It depends on the question. A quick lookup can finish in seconds; a question that sends the agent through several connectors takes longer. Streaming shows progress as it happens, and a stream stays open for up to 10 minutes.
Does asking the agent cost anything?
Yes, the same as asking in the app: the work the agent does uses NTUs. The API request itself isn't metered separately. To share an answer with the team, post its output_text with Post a message.
Can I retry a request that timed out?
Retrying Ask the agent starts a new answer, and it doesn't accept an Idempotency-Key. If you got a response id back, retrieve it instead. If you didn't, the answer may never have started; asking again is safe, but may use NTUs twice.
Why is output_text null?
The answer isn't finished yet, or it didn't complete. output_text is set only when status is completed; for failed, read error.message.
Can I read answers people got from the agent in the app?
No. Only responses started through the API are readable through it. Retrieving the ID of any other agent answer fails with 404 resource_missing.