---
title: Create research
description: Start an answer that runs on its own, and read it later.
doc_version: 0.1.0-preview
last_updated: 2026-09-14
---

# Create research

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

Start an answer that runs on its own, and 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, in any mode. recall, web_search use_if_needed, and query_image need a cluster key.

The run starts queued. It moves to running, then ends succeeded, failed, or canceled. The limits of Answer hold: 120 seconds for standard, 600 for ultra and recall, 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.",
  "mode": "ultra",
  "return_sources": true
}
```

## 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`

**FilterClause · optional · nullable**

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

- Maximum depth: 8
- Maximum clauses: 100
- An in list holds 1–100 values.

#### Comparison

Compare one built-in or metadata field.

##### `filter.field`

**string · required**

path, content_type, bytes, created_at, updated_at, status, or metadata.<key>. Metadata keys have 1–256 characters.

##### `filter.op`

**string · required**

Comparison operator. Strings support eq, neq, prefix, and in. bytes supports numeric comparisons and in. Timestamps support comparisons. status supports eq, neq, and in.

- `eq`: Equal.
- `neq`: Not equal.
- `gt`: Greater than.
- `gte`: Greater than or equal.
- `lt`: Less than.
- `lte`: Less than or equal.
- `prefix`: String starts with this value.
- `in`: Equal to one value in a nonempty array.

##### `filter.value`

**JSON scalar | array<JSON scalar> · required**

Value of the correct type for the field. bytes uses non-negative integers. Timestamps need an ISO 8601 timezone. in takes an array. Metadata equality accepts string, number, boolean, or null.

#### All clauses

##### `filter.and`

**array<FilterClause> · required**

Nonempty list of clauses that must all match. Each entry uses this same FilterClause grammar.

#### Any clause

##### `filter.or`

**array<FilterClause> · required**

Nonempty list of clauses where at least one must match. Each entry uses this same FilterClause grammar.

#### Negated clause

##### `filter.not`

**FilterClause · required**

One clause that must not match. It uses this same FilterClause grammar.

### `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.

### `mode`

**string · optional**

Answer mode. standard performs one retrieve-and-answer pass. ultra performs deeper work within the collection. Check GET /info for supported modes.

Default: `"standard"`.

- `standard`: One retrieve-and-answer pass, with a 120-second limit.
- `ultra`: Deeper work within the collection, with a 600-second limit.
- `recall`: Deeper work that tries to find every match, with a 600-second limit. Needs a cluster key.

### `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.

### `return_sources`

**boolean · optional**

Include the source map for citations. When false, the response omits sources.

Default: `true`.

### `web_search`

**string · optional**

Whether the answer may use web search: never (default) or use_if_needed. use_if_needed needs a cluster key, and web search runs only in ultra when the deployment enables it.

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 ultra.

## Responses

### 202 · Accepted

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

Content type: `application/json`.

#### Response headers

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

**string · required**

Request correlation ID. Retain it when investigating a failed or unexpected request.

```json
{
  "id": "rsr_01K6Q6N7D8E9F0G1H2J3K4M5N6",
  "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-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. Citation markers use [[SRC:0001]] and match the [SRC:0001] keys in sources. 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.

##### `mode`

**string · required**

Answer mode of the run.

- `standard`: One retrieve-and-answer pass, with a 120-second limit.
- `ultra`: Deeper work within the collection, with a 600-second limit.
- `recall`: Deeper work that tries to find every match, with a 600-second limit. Needs a cluster key.

##### `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`

**map<string, object> · required · nullable**

Map from citation markers to supporting sources, once the run succeeds. Keys omit the marker’s outer brackets. Null when return_sources was false.

###### `sources[key].evidence`

**object · required**

One source excerpt with its kind and optional position.

###### `sources[key].evidence.kind`

**string · required**

Form of the extracted evidence, such as text, transcript, OCR, or a page.

- `text`: Extracted text.
- `transcript`: Transcript from audio or video.
- `ocr`: Text recognized from an image or document page.
- `json_path`: Evidence at a JSON path.
- `page`: Evidence from a document page.
- `image_region`: Evidence from a region of an image.

###### `sources[key].evidence.location`

**object · optional · nullable**

Position of this evidence within the source. The fields depend on its location type. Omitted when no position is available.

###### TimeSpan

###### `sources[key].evidence.location.end_ms`

**integer · required**

End of the source excerpt, in milliseconds from the beginning of the audio or video.

###### `sources[key].evidence.location.start_ms`

**integer · required**

Start of the source excerpt, in milliseconds from the beginning of the audio or video.

###### Page

###### `sources[key].evidence.location.bbox`

**array<number> · optional · nullable**

Optional bounding box coordinates for a region on the referenced page.

###### `sources[key].evidence.location.page`

**integer · required**

Page number identifying the evidence in the source.

###### JsonPath

###### `sources[key].evidence.location.json_path`

**string · required**

JSON path locating the evidence within a structured source.

###### CharSpan

###### `sources[key].evidence.location.char_end`

**integer · required**

End character offset of the evidence within extracted text.

###### `sources[key].evidence.location.char_start`

**integer · required**

Start character offset of the evidence within extracted text.

###### `sources[key].evidence.text`

**string · required**

Extracted source text supporting this match or citation.

###### `sources[key].file_id`

**string · required**

ID of the source file within the queried collection.

###### `sources[key].is_cited`

**boolean · required**

Whether the answer cites this source marker.

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

**string · required**

Logical path of the source file within the collection.

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

**number · required**

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

##### `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.

Use the documented filter grammar and field-specific value types.

```json
{
  "error": {
    "code": "invalid_filter",
    "message": "A filter uses an unsupported field, operator, value, or nesting structure.",
    "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 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 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 x-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.",
  "mode": "ultra",
  "return_sources": true
}'
```

### 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.", "mode": "ultra", "return_sources": True}).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.","mode":"ultra","return_sources":true}),
});
console.log(response.status, await response.text());
```

## Sitemap

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