Research
AI DraftStart an answer that runs on its own, follow its progress, and read or cancel it.
Start an answer that runs on its own, and read it when it ends. A research run takes the same request as Answer, but no connection stays open while Engine works.
When to use research
Answer holds the HTTP connection open until the answer is ready. If the client disconnects, Engine cancels the query. Research returns 202 at once with a run ID. Engine then works on the answer by itself and reports each change.
| Operation | Use it when |
|---|---|
A person waits for the reply, and the question fits in one request. | |
Research | The question can take minutes, as in |
Both operations take the same body, run the same checks, and have the same time limits. An image in query_image adds 45 seconds. Both need the key’s query action.
Before you begin
Use a collection with ready files and a key with query access. Check query.modes in /info for the modes that your cluster supports. recall, web_search: "use_if_needed", and query_image need a cluster key.
To follow runs with events, ask the cluster operator to add research.updated and research.progress to a webhook. See Handle jobs and events. Without a webhook, read the run with GET.
Start a run
POST https://engine.example.com/namespaces/acme/collections/incident-logs/research
POST /namespaces/acme/collections/incident-logs/research HTTP/1.1
Host: engine.example.com
Authorization: Bearer <GRAPHON_ENGINE_API_KEY>
Content-Type: application/json
Idempotency-Key: checkout-pool-research-1
{
"query": "Find every incident in which checkout ran out of database connections.",
"mode": "ultra",
"return_sources": true
}202 Accepted
{
"id": "rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
"namespace": "acme",
"collection": "incident-logs",
"status": "queued",
"mode": "ultra",
"progress": null,
"answer": null,
"sources": null,
"image_description": null,
"usage": null,
"error": null,
"created_at": "2026-10-06T12:02:00Z",
"updated_at": "2026-10-06T12:02:00Z",
"completed_at": null
}Save id. It starts with rsr_. You need it to read or cancel the run. The run starts as queued. A field that the run has not set yet is null.
Idempotency-Key is required. A repeat with the same key and body returns the same run in its current state. A different body with the same key returns 409 idempotency_conflict. Engine keeps the key for 24 hours. Use a new key for each new run.
Engine checks the request and the collection before it creates the run. A failed check returns its error at once, and Engine creates no run. See Handle errors.
Follow the run
| Status | Meaning |
|---|---|
| Engine accepted the run. The run has not started. |
| Engine is working on the answer. |
| The answer is ready. |
| The run ended with an error. Read |
| The run stopped after a cancel. |
succeeded, failed, and canceled are final. A run in a final status does not change again.
Engine sends research.updated when it creates the run and at each status change. The first event has data.previous_status: null. Engine sends research.progress each time progress changes. Each research event names the run in research_id. Its job_id and file_id are null.
Example webhook body
{
"events": [
{
"id": "evt_01J8Z3K4N5P6Q7R8S9T0V1W2X4",
"type": "research.progress",
"occurred_at": "2026-10-06T12:04:00Z",
"cluster": "graphon-engine",
"namespace": "acme",
"collection": "incident-logs",
"job_id": null,
"file_id": null,
"research_id": "rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
"data": {
"stage": "synthesizing",
"label": "Synthesizing answer",
"step": 3,
"total_steps": 3
}
},
{
"id": "evt_01J8Z3K4N5P6Q7R8S9T0V1W2X5",
"type": "research.updated",
"occurred_at": "2026-10-06T12:05:00Z",
"cluster": "graphon-engine",
"namespace": "acme",
"collection": "incident-logs",
"job_id": null,
"file_id": null,
"research_id": "rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
"data": {
"status": "succeeded",
"previous_status": "running",
"mode": "ultra"
}
}
]
}A research.progress event has stage, label, step, and total_steps in data. Show label to people, and use stage in code. A research.updated event has status, previous_status, and mode, and error_code when the status is failed. Deduplicate each event by its id.
Each webhook has its own delivery order. A separate webhook for research events keeps them from waiting behind file events.
Read the run
Read the run at any time with GET. Use it when you have no webhook, after a missed event, or to load the answer after research.updated reports succeeded.
GET https://engine.example.com/namespaces/acme/collections/incident-logs/research/rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3
GET /namespaces/acme/collections/incident-logs/research/rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3 HTTP/1.1
Host: engine.example.com
Authorization: Bearer <GRAPHON_ENGINE_API_KEY>200 OK · running
{
"id": "rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
"namespace": "acme",
"collection": "incident-logs",
"status": "running",
"mode": "ultra",
"progress": {
"stage": "exploring",
"label": "Exploring your files",
"step": 2,
"total_steps": 3
},
"answer": null,
"sources": null,
"image_description": null,
"usage": null,
"error": null,
"created_at": "2026-10-06T12:02:00Z",
"updated_at": "2026-10-06T12:03:00Z",
"completed_at": null
}If you poll, wait a few seconds between reads. Engine checks the stage of a running run every 5 seconds, so faster reads return the same progress.
Only the key that started the run, or a cluster key, can read or cancel it. Another key gets 404 not_found, as for a run that does not exist. The key must also reach the collection with the query action.
Read the answer and sources
200 OK · succeeded
{
"id": "rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
"namespace": "acme",
"collection": "incident-logs",
"status": "succeeded",
"mode": "ultra",
"progress": {
"stage": "synthesizing",
"label": "Synthesizing answer",
"step": 3,
"total_steps": 3
},
"answer": "One incident matches: checkout exhausted its connection pool [[SRC:0001]].",
"sources": {
"[SRC:0001]": {
"file_id": "fil_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
"path": "/incidents/checkout.txt",
"score": 0.81,
"is_cited": true,
"evidence": {
"kind": "text",
"text": "Checkout stalled because the service exhausted its connection pool.",
"location": {
"char_start": 0,
"char_end": 67
}
}
}
},
"image_description": null,
"usage": {
"credits": 4
},
"error": null,
"created_at": "2026-10-06T12:02:00Z",
"updated_at": "2026-10-06T12:05:00Z",
"completed_at": "2026-10-06T12:05:00Z"
}When the status is succeeded, the run holds answer, sources, image_description, and usage. These fields have the same shape as in an Answer response. usage.credits is the final credit count for the run. completed_at is the time the run ended.
Render citations as for Answer. [[SRC:0001]] in answer maps to the [SRC:0001] key in sources. See Render citations. sources is null when the request set return_sources: false.
The sources include only files that are still in the collection when the run ends. Engine removes a source whose file is gone, together with its markers.
Cancel a run
POST https://engine.example.com/namespaces/acme/collections/incident-logs/research/rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3:cancel
POST /namespaces/acme/collections/incident-logs/research/rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3:cancel HTTP/1.1
Host: engine.example.com
Authorization: Bearer <GRAPHON_ENGINE_API_KEY>202 Accepted · cancel requested
{
"id": "rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
"namespace": "acme",
"collection": "incident-logs",
"status": "running",
"mode": "ultra",
"progress": {
"stage": "exploring",
"label": "Exploring your files",
"step": 2,
"total_steps": 3
},
"answer": null,
"sources": null,
"image_description": null,
"usage": null,
"error": null,
"created_at": "2026-10-06T12:02:00Z",
"updated_at": "2026-10-06T12:03:00Z",
"completed_at": null
}A queued run becomes canceled at once. A running run can still show running in the response. It becomes canceled when Engine has stopped its work. Engine does not bill a canceled run.
Wait for research.updated with data.status: "canceled", or read the run. A cancel of a run that already ended returns the run unchanged.
Handle errors
A request error returns at once, and Engine creates no run. The checks are the same as for Answer.
| Error | Cause |
|---|---|
| A field is unknown or invalid, the mode is not available, or |
| The |
| The key does not have the |
| The key cannot see the collection, or the collection is being deleted. |
| The collection has no files. |
| No content is ready. When |
| The |
| The body is larger than 6,291,456 bytes. |
| A dependency is not available. Retry with the same |
A run that starts can still end failed. Its error then holds code, message, and retryable. The research.updated event holds the code in data.error_code.
200 OK · failed
{
"id": "rsr_01J8Z3K4N5P6Q7R8S9T0V1W2X3",
"namespace": "acme",
"collection": "incident-logs",
"status": "failed",
"mode": "ultra",
"progress": {
"stage": "exploring",
"label": "Exploring your files",
"step": 2,
"total_steps": 3
},
"answer": null,
"sources": null,
"image_description": null,
"usage": null,
"error": {
"code": "temporarily_unavailable",
"message": "The research ran longer than 600 seconds. It is not billed. Start it again.",
"retryable": true
},
"created_at": "2026-10-06T12:02:00Z",
"updated_at": "2026-10-06T12:12:00Z",
"completed_at": "2026-10-06T12:12:00Z"
}A run that passes its time limit ends failed with temporarily_unavailable, and Engine does not bill it. A run that the cluster stops, for example during a deploy, ends the same way. When retryable is true, start a new run with a new Idempotency-Key. The same key returns the same failed run.
Retention
Engine keeps a run for 7 days after it ends. A daily cleanup then deletes it, and a read returns 404 not_found. Store the answer and sources in your application if you need them for longer.
See Create research, Get research, and Cancel research for every field and error.