Agent

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: true and 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.