---
title: Create research
description: Start a research run: deep, many-step work that runs on its own.
doc_version: 0.1.0-preview
last_updated: 2026-09-14
---

# Create research

`POST /namespaces/{namespace}/collections/{collection}/research`

Start a research run: deep, many-step work that runs on its own. Read it later.

**Permission:** Cluster key, or a scoped key with query on this collection.

[API key types and authentication](/engine/reference/authentication.md).

Send Idempotency-Key. Retry the same logical request with the same key and body. Idempotency records remain available for replay for 24 hours.

The body and its checks are those of Answer, with no effort. web_search use_if_needed and query_image need a cluster key. Web search runs only in a research run.

The run starts queued. It moves to running, then ends succeeded, failed, or canceled. A run allows 600 seconds, and 45 more with an image.

research.updated reports each status change, and research.progress reports each new stage. Each webhook POST holds a batch of events.

Only this key, or a cluster key, can read or cancel the run.

## Example request

```http
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: 8f3c1e2a-4b5d-6e7f-8091-a2b3c4d5e6f7

{
  "query": "Find every incident in which checkout ran out of database connections."
}
```

## Path parameters

### `namespace`

**string · required**

Namespace name or ns_ ID. It must be visible to this key.

### `collection`

**string · required**

Collection name or col_ ID within the namespace. It must be visible to this key.

## Headers

### `Idempotency-Key`

**string · required**

Unique key for this logical operation. Reuse it only with the same request for retries. Retained for 86,400 seconds. A changed body returns idempotency_conflict.

## Request body

### `filter`

**map<string, JSON value> · optional · nullable**

Structured selection of files to use for the answer. Uses the same grammar as Search. Filters do not replace access rules.

### `history`

**array<object> · optional**

Prior conversation turns, oldest first. Maximum 32 messages. Your application stores and resends this history.

- Maximum items: 32

#### `history[].content`

**string · required**

Text of this previous conversation turn. Maximum 32,000 characters.

- Maximum characters: 32000

#### `history[].role`

**string · required**

Speaker of this prior turn: user or assistant. Engine does not store a conversation.

- `user`: A previous user message.
- `assistant`: A previous assistant answer.

### `instructions`

**string · optional · nullable**

Style instructions for the answer, such as its language, tone, or format. At most 8,000 characters. They replace the default answer style, not the rules for using sources.

- Maximum characters: 8000

### `principals`

**array<string> · optional · nullable**

End-user and group identifiers supplied by your server. Required on enforced collections and invalid when access control is off. An empty list gives no results. Maximum 4,096 entries, each at most 256 UTF-8 bytes.

### `query`

**string · required**

Question to answer from this collection. Must contain non-space text and be at most 32,000 characters.

- Minimum characters: 1
- Maximum characters: 32000

### `query_image`

**object · optional · nullable**

An image to ask about, read for this query only. Engine does not store or index it. Needs a cluster key, and adds 45 seconds to the time limit.

#### `query_image.data`

**string · required**

The image as base64 with no data-URI prefix. It decodes to at most 4 MiB.

#### `query_image.media_type`

**string · required**

Image format: image/png, image/jpeg, or image/webp.

- `image/png`: PNG image.
- `image/jpeg`: JPEG image.
- `image/webp`: WebP image.

### `sources`

**string · optional**

Which sources the response lists: all (default), cited, or none. none also removes the citation markers from the answer.

Default: `"all"`.

- `all`: List the cited sources first, then the other retrieved sources.
- `cited`: List only the cited sources.
- `none`: List no sources, and remove the citation markers from the answer.

### `web_search`

**string · optional**

Whether the run may use web search: never (default) or use_if_needed. use_if_needed needs a cluster key, and the deployment must enable web search.

Default: `"never"`.

- `never`: Do not use web search.
- `use_if_needed`: Use web search when the collection cannot answer. Needs a cluster key, and runs only in a research run.

## Responses

### 202 · Accepted

The run is queued. Read it with Get research, or wait for research.updated.

Content type: `application/json`.

#### Response headers

##### `x-engine-request-id`

**string · required**

The Engine request ID. Retain it when investigating a failed or unexpected request.

##### `x-request-id`

**string · required**

The X-Request-Id you sent, echoed when it is a valid ID. Otherwise, the Engine request ID.

```json
{
  "id": "rsr_01K6Q6N7D8E9F0G1H2J3K4M5N6",
  "namespace": "acme",
  "collection": "incident-logs",
  "status": "queued",
  "progress": null,
  "answer": null,
  "sources": null,
  "image_description": null,
  "usage": null,
  "error": null,
  "created_at": "2026-10-06T10:00:00Z",
  "updated_at": "2026-10-06T10:00:00Z",
  "completed_at": null
}
```

#### Response fields

##### `answer`

**string · required · nullable**

Answer text, once the run succeeds. A citation marker such as [[SRC:0001]] names the source whose ref is SRC:0001. The answer has no markers when the request set sources to none. Otherwise null.

##### `collection`

**string · required**

Name of the collection that the run answers from.

##### `completed_at`

**string (date-time) · required · nullable**

Time the run ended, as an ISO 8601 UTC timestamp. Null while it runs.

##### `created_at`

**string (date-time) · required**

Time the resource was created, as an ISO 8601 UTC timestamp.

##### `error`

**object · required · nullable**

Why the run failed, with retryable. Null unless the run failed.

###### `error.code`

**string · required**

Machine-readable reason for the failure. Use this value for program logic and message for display or diagnostics.

###### `error.message`

**string · required**

Human-readable explanation of the failure. Do not parse this text to decide how to retry.

###### `error.retryable`

**boolean · required**

Whether starting the same research again may succeed.

##### `id`

**string · required**

Immutable research run ID with the rsr_ prefix. Use it to read or cancel the run.

##### `image_description`

**string · required · nullable**

How Engine read the query image, once the run succeeds. Null without an image.

##### `namespace`

**string · required**

Name of the namespace of the collection.

##### `progress`

**object · required · nullable**

The last stage of the run, or null before the first one.

###### `progress.label`

**string · required**

Readable name of the stage, for display.

###### `progress.stage`

**string · required**

Machine-readable name of the stage.

###### `progress.step`

**integer · required**

Number of this stage, from 1.

###### `progress.total_steps`

**integer · required**

Number of stages that Engine expects for this run.

##### `sources`

**array<object> · required · nullable**

The sources in ref order, once the run succeeds: the cited sources first, then the others by score. Empty when the request set sources to none. Otherwise null.

###### `sources[].cited`

**boolean · required**

Whether the answer cites this source.

###### `sources[].content_type`

**string · required**

Content type of the source file.

###### `sources[].file`

**object · optional · nullable**

Not returned yet. The whole stored file.

###### `sources[].file.content_type`

**string · required**

Content type of the part.

###### `sources[].file.data`

**string · optional · nullable**

The bytes, as base64. Present when the part is inline.

###### `sources[].file.expires_at`

**string (date-time) · optional · nullable**

ISO 8601 UTC time after which the URL expires.

###### `sources[].file.page`

**integer · optional · nullable**

Not returned yet. The page of a page screenshot, from 1.

###### `sources[].file.seconds`

**number · optional · nullable**

Not returned yet. The time of a video frame, in seconds from the start.

###### `sources[].file.size_bytes`

**integer · required**

Size of the part in bytes, before base64.

###### `sources[].file.url`

**string · optional · nullable**

Signed URL to the bytes. It works for 15 minutes. On a client deployment it is a gs:// URI. Present when the part did not fit inline, or delivery is url.

###### `sources[].matches`

**array<object> · optional · nullable**

Not returned yet. On a cited source, one match: the passage that supports the answer.

###### `sources[].matches[].highlight`

**object · optional · nullable**

The text around the match, and where the matched words are.

###### `sources[].matches[].highlight.match_ranges`

**array<[integer, integer]> · required**

The [start, end) code point ranges of the matched words in text.

###### `sources[].matches[].highlight.text`

**string · required**

About 300 characters around the match.

###### `sources[].matches[].raw`

**object · optional · nullable**

The match cut from the file in its own format.

###### `sources[].matches[].raw.content_type`

**string · required**

Content type of the part.

###### `sources[].matches[].raw.data`

**string · optional · nullable**

The bytes, as base64. Present when the part is inline.

###### `sources[].matches[].raw.expires_at`

**string (date-time) · optional · nullable**

ISO 8601 UTC time after which the URL expires.

###### `sources[].matches[].raw.page`

**integer · optional · nullable**

Not returned yet. The page of a page screenshot, from 1.

###### `sources[].matches[].raw.seconds`

**number · optional · nullable**

Not returned yet. The time of a video frame, in seconds from the start.

###### `sources[].matches[].raw.size_bytes`

**integer · required**

Size of the part in bytes, before base64.

###### `sources[].matches[].raw.url`

**string · optional · nullable**

Signed URL to the bytes. It works for 15 minutes. On a client deployment it is a gs:// URI. Present when the part did not fit inline, or delivery is url.

###### `sources[].matches[].screenshots`

**array<object> · optional · nullable**

Images of the match.

###### `sources[].matches[].screenshots[].content_type`

**string · required**

Content type of the part.

###### `sources[].matches[].screenshots[].data`

**string · optional · nullable**

The bytes, as base64. Present when the part is inline.

###### `sources[].matches[].screenshots[].expires_at`

**string (date-time) · optional · nullable**

ISO 8601 UTC time after which the URL expires.

###### `sources[].matches[].screenshots[].page`

**integer · optional · nullable**

Not returned yet. The page of a page screenshot, from 1.

###### `sources[].matches[].screenshots[].seconds`

**number · optional · nullable**

Not returned yet. The time of a video frame, in seconds from the start.

###### `sources[].matches[].screenshots[].size_bytes`

**integer · required**

Size of the part in bytes, before base64.

###### `sources[].matches[].screenshots[].url`

**string · optional · nullable**

Signed URL to the bytes. It works for 15 minutes. On a client deployment it is a gs:// URI. Present when the part did not fit inline, or delivery is url.

###### `sources[].matches[].span`

**object · required**

The place of the match, inside the span of its result.

###### `sources[].matches[].span.bbox`

**object · optional · nullable**

Not returned yet. The box of a figure on one page. Read spans accepts it and reads the whole page.

###### `sources[].matches[].span.bbox.bottom`

**number · required**

The bottom edge, as a fraction of the page height from the top.

- Minimum: 0
- Maximum: 1

###### `sources[].matches[].span.bbox.left`

**number · required**

The left edge, as a fraction of the page width from the left.

- Minimum: 0
- Maximum: 1

###### `sources[].matches[].span.bbox.right`

**number · required**

The right edge, as a fraction of the page width from the left.

- Minimum: 0
- Maximum: 1

###### `sources[].matches[].span.bbox.top`

**number · required**

The top edge, as a fraction of the page height from the top.

- Minimum: 0
- Maximum: 1

###### `sources[].matches[].span.end`

**integer · optional · nullable**

First unit after the span. Absent when unit is file.

- Minimum: 0
- Maximum: 2147483647

###### `sources[].matches[].span.file_id`

**string · required**

ID of the file within the collection.

###### `sources[].matches[].span.pages`

**object · optional · nullable**

The pages that a chars span of a PDF or Office file covers, from 1, so a client can show a page number. Absent on every other span. Read spans refuses pages that do not match the chars.

###### `sources[].matches[].span.pages.end`

**integer · required**

First page after the range.

- Minimum: 1

###### `sources[].matches[].span.pages.start`

**integer · required**

First page of the range, from 1.

- Minimum: 1

###### `sources[].matches[].span.start`

**integer · optional · nullable**

First unit of the span. Pages count from 1. Chars and seconds count from 0. Absent when unit is file.

- Minimum: 0
- Maximum: 2147483647

###### `sources[].matches[].span.unit`

**string · required**

pages (a section or a figure of a PDF or Office file), chars (Unicode code points of the text form), seconds (whole seconds), or file (the whole file).

- `pages`: Pages of a PDF or an office file, from 1. Page 7 is start 7, end 8.
- `chars`: Unicode code points of the text form of a file, from 0: a txt, md, or mail file, or a PDF or office file.
- `seconds`: Whole seconds of a video or an audio file, from 0.
- `file`: The whole file, with no start or end.

###### `sources[].matches[].text`

**string · optional · nullable**

The text of the match.

###### `sources[].path`

**string · required**

Logical path of the source file within the collection.

###### `sources[].ref`

**string · required**

The marker of this source without its brackets, such as SRC:0001. It stands in answer as [[SRC:0001]].

###### `sources[].score`

**number · required**

Ranking value for this result. It is not a confidence percentage.

###### `sources[].span`

**object · required**

Place in the source file. Send it to Read spans to get its content.

###### `sources[].span.bbox`

**object · optional · nullable**

Not returned yet. The box of a figure on one page. Read spans accepts it and reads the whole page.

###### `sources[].span.bbox.bottom`

**number · required**

The bottom edge, as a fraction of the page height from the top.

- Minimum: 0
- Maximum: 1

###### `sources[].span.bbox.left`

**number · required**

The left edge, as a fraction of the page width from the left.

- Minimum: 0
- Maximum: 1

###### `sources[].span.bbox.right`

**number · required**

The right edge, as a fraction of the page width from the left.

- Minimum: 0
- Maximum: 1

###### `sources[].span.bbox.top`

**number · required**

The top edge, as a fraction of the page height from the top.

- Minimum: 0
- Maximum: 1

###### `sources[].span.end`

**integer · optional · nullable**

First unit after the span. Absent when unit is file.

- Minimum: 0
- Maximum: 2147483647

###### `sources[].span.file_id`

**string · required**

ID of the file within the collection.

###### `sources[].span.pages`

**object · optional · nullable**

The pages that a chars span of a PDF or Office file covers, from 1, so a client can show a page number. Absent on every other span. Read spans refuses pages that do not match the chars.

###### `sources[].span.pages.end`

**integer · required**

First page after the range.

- Minimum: 1

###### `sources[].span.pages.start`

**integer · required**

First page of the range, from 1.

- Minimum: 1

###### `sources[].span.start`

**integer · optional · nullable**

First unit of the span. Pages count from 1. Chars and seconds count from 0. Absent when unit is file.

- Minimum: 0
- Maximum: 2147483647

###### `sources[].span.unit`

**string · required**

pages (a section or a figure of a PDF or Office file), chars (Unicode code points of the text form), seconds (whole seconds), or file (the whole file).

- `pages`: Pages of a PDF or an office file, from 1. Page 7 is start 7, end 8.
- `chars`: Unicode code points of the text form of a file, from 0: a txt, md, or mail file, or a PDF or office file.
- `seconds`: Whole seconds of a video or an audio file, from 0.
- `file`: The whole file, with no start or end.

##### `status`

**string · required**

queued, running, succeeded, failed, or canceled. succeeded, failed, and canceled are terminal states.

- `queued`: Accepted and waiting to start.
- `running`: Engine is working on the answer.
- `succeeded`: The answer is ready.
- `failed`: The run ended with an error.
- `canceled`: The run ended after cancellation.

##### `updated_at`

**string (date-time) · required**

Time the resource last changed, as an ISO 8601 UTC timestamp.

##### `usage`

**object · required · nullable**

What the run used, once it succeeds. Otherwise null.

###### `usage.credits`

**integer · required**

Credits metered for this query.

## Errors

[Error format and retry guidance](/engine/reference/errors.md).

### 400 · `invalid_request`

A field, path, parameter, or conditional request rule is invalid.

Correct the request. Unknown fields are rejected.

```json
{
  "error": {
    "code": "invalid_request",
    "message": "A field, path, parameter, or conditional request rule is invalid.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 400 · `invalid_filter`

A filter uses an unsupported field, operator, value, or nesting structure, or a metadata key that the collection does not list. details.field names the field. details.reason is not_listed, op_not_allowed, or invalid_value for a metadata field.

Use the documented filter grammar and field-specific value types. List a metadata key with Update metadata fields before you filter on it.

```json
{
  "error": {
    "code": "invalid_filter",
    "message": "A filter uses an unsupported field, operator, value, or nesting structure, or a metadata key that the collection does not list. details.field names the field. details.reason is not_listed, op_not_allowed, or invalid_value for a metadata field.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 401 · `unauthenticated`

The API key is missing, invalid, or revoked.

Send an active key in Authorization: Bearer or X-API-Key.

```json
{
  "error": {
    "code": "unauthenticated",
    "message": "The API key is missing, invalid, or revoked.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 403 · `forbidden`

The key can see the resource but lacks permission for this operation.

Use a key with the required action or scope kind.

```json
{
  "error": {
    "code": "forbidden",
    "message": "The key can see the resource but lacks permission for this operation.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 404 · `not_found`

The resource is absent, deleted, or outside this key’s scope.

Check the identifier and the key’s namespace and collection scope.

```json
{
  "error": {
    "code": "not_found",
    "message": "The resource is absent, deleted, or outside this key’s scope.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 409 · `idempotency_conflict`

This idempotency key was already used with a different request body.

Reuse the original body for a retry, or choose a new key for a new operation.

```json
{
  "error": {
    "code": "idempotency_conflict",
    "message": "This idempotency key was already used with a different request body.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 409 · `collection_empty`

The collection contains no living files to answer from.

Add a file and wait for a ready event before querying.

```json
{
  "error": {
    "code": "collection_empty",
    "message": "The collection contains no living files to answer from.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 409 · `collection_not_ready`

The collection cannot serve this retrieval request yet.

Read retryable and details.retry_after_seconds. If processing failed, inspect file errors and correct the source.

```json
{
  "error": {
    "code": "collection_not_ready",
    "message": "The collection cannot serve this retrieval request yet.",
    "request_id": "req_example",
    "retryable": true,
    "details": {
      "retry_after_seconds": 30
    }
  }
}
```

### 413 · `request_too_large`

The JSON body exceeds the route limit: 6,291,456 bytes on :answer and research, 4,194,304 bytes on :search and :read-spans, and 1,048,576 bytes elsewhere.

Reduce the JSON body. Use upload or a URI source for larger file content.

```json
{
  "error": {
    "code": "request_too_large",
    "message": "The JSON body exceeds the route limit: 6,291,456 bytes on :answer and research, 4,194,304 bytes on :search and :read-spans, and 1,048,576 bytes elsewhere.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 503 · `temporarily_unavailable`

A dependency, cluster readiness condition, or capacity limit prevents this operation.

Inspect retryable and details. Retry only when permitted, preserving the idempotency key for the same write.

```json
{
  "error": {
    "code": "temporarily_unavailable",
    "message": "A dependency, cluster readiness condition, or capacity limit prevents this operation.",
    "request_id": "req_example",
    "retryable": true,
    "details": {}
  }
}
```

### 500 · `internal_error`

An unexpected server failure prevented this request from completing.

Retain request_id for investigation. Follow retryable before retrying a write.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An unexpected server failure prevented this request from completing.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

## Other request languages

### cURL

```bash
curl -sS -X POST "$GRAPHON_ENGINE_URL/namespaces/acme/collections/incident-logs/research" \
  -H "Authorization: Bearer $GRAPHON_ENGINE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: 8f3c1e2a-4b5d-6e7f-8091-a2b3c4d5e6f7' \
  --data-raw '{
  "query": "Find every incident in which checkout ran out of database connections."
}'
```

### Python

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    os.environ["GRAPHON_ENGINE_URL"] + "/namespaces/acme/collections/incident-logs/research",
    data=json.dumps({"query": "Find every incident in which checkout ran out of database connections."}).encode("utf-8"),
    headers={
        "Authorization": "Bearer " + os.environ["GRAPHON_ENGINE_API_KEY"],
        "Content-Type": "application/json",
        "Idempotency-Key": "8f3c1e2a-4b5d-6e7f-8091-a2b3c4d5e6f7",
    },
    method="POST",
)
with urllib.request.urlopen(request) as response:
    print(response.read().decode())
```

### JavaScript

```javascript
const response = await fetch(process.env.GRAPHON_ENGINE_URL + "/namespaces/acme/collections/incident-logs/research", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.GRAPHON_ENGINE_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "8f3c1e2a-4b5d-6e7f-8091-a2b3c4d5e6f7",
  },
  body: JSON.stringify({"query":"Find every incident in which checkout ran out of database connections."}),
});
console.log(response.status, await response.text());
```

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
