---
title: "Fork a session"
method: POST
path: "/v1/projects/{project_id}/sessions/{session_id}/fork"
tags: ["Sessions"]
---

# Fork a session

`POST /v1/projects/{project_id}/sessions/{session_id}/fork`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/sessions/{session_id}/fork`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.naturali.ai/authorize`, token URL `https://api.naturali.ai/token`, scopes: `mcp:access`)

Branches a new session from a point in this session's history: same context, different continuation.

The fork gets its own conversation whose messages **reference the same documents** as the parent rather than copying them, so there is one stored copy of the content and a retention purge erases it from both. Recorded tool results ride along on those messages and are **replayed** as model input on the fork's next turn — forking never re-invokes a tool, so exploring a "what if" cannot send an email or charge a card a second time. The consequence to accept is that a forked turn sees the tool data as it was, not as it is now.

The fork is created **inert**: `auto_generate` is false and no generation is triggered. Drive it with the normal message and generate endpoints. The fork has no actor — attach one only if the branch is meant to be driven by the same end user, since `single_session_per_actor` agents allow one open session per actor.

## Path parameters

- `session_id` string, required

## Request body

- ForkSessionRequest
  - `fork_at_position` integer: The parent conversation `position` to branch after. Messages at positions 0..N are carried into the fork. Omit it to branch at the tip (the whole history).
  - `agent_id` string: Agent the fork runs against. Defaults to the parent session's agent; overriding it is the point of forking — same context, a different agent or agent version. Must belong to the same project as the session being forked.
  - `name` string: Optional name for the forked session
  - `tags` TagBag: Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search. Keys beginning `system.` are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag. The bag is bounded, because every pair reaches the IAM context of every access check on the resource: at most 50 keys, each key at most 128 characters and each value at most 256. A write past a bound — including a merge that would grow the stored bag past the key count — is `400 VALIDATION_FAILED` with `meta.limit` naming the bound it crossed. `system.*` keys are the platform's and do not count against the 50.
  - `tool_context` object, nullable: Overrides the parent's `tool_context` on the fork. Omit it and the fork inherits the parent's, so the branch is faithful to the run it came from.

## Response `201`

Fork created

- SessionRecord
  - `id` string: Session public ID
  - `agent_id` string: Agent public ID, kept after a shared agent is deleted by its owner.
  - `conversation_id` string: Underlying conversation public ID
  - `status` 'open' | 'closed' | 'expired'
  - `name` string, nullable
  - `actor_id` string, nullable: Public ID of the user actor, or null when the session was created without one
  - `tags` TagBag: Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search. Keys beginning `system.` are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag. The bag is bounded, because every pair reaches the IAM context of every access check on the resource: at most 50 keys, each key at most 128 characters and each value at most 256. A write past a bound — including a merge that would grow the stored bag past the key count — is `400 VALIDATION_FAILED` with `meta.limit` naming the bound it crossed. `system.*` keys are the platform's and do not count against the 50.
  - `auto_generate` boolean: When true, automatically triggers generation after each user message (if no generation is in progress).
  - `generating_at` string, date-time, nullable: Timestamp when the current generation started, or null if not generating.
  - `created_at` string, date-time
  - `updated_at` string, date-time
  - `inactivity_ttl_seconds` integer: Number of seconds of inactivity after which the session expires. 0 means the session never expires.
  - `message_delay_seconds` integer, nullable: Number of seconds to wait after the last user message before sending to the LLM. Acts as a debounce: each new message resets the timer. null or absent means no delay (immediate processing).
  - `max_context_messages` integer, nullable: Maximum number of recent conversation messages sent to the model on each generation. Takes precedence over the agent's `max_context_messages`; null defers to it. A fork keeps its parent's value.
  - `last_activity_at` string, date-time, nullable: Timestamp of the last activity on the session (message added or response generated).
  - `forked_from_session_id` string, nullable: Public ID of the session this one was forked from, or null when it was not forked. Also null once that parent is deleted — a fork survives its parent and keeps its own history.
  - `forked_from_position` integer, nullable: The parent conversation position this session branched after, or null when it is not a fork or was forked at the tip.
  - `usage` UsageTotals: Token counts and cost for one slice of the meter. Tokens and cost only: a `compute_second` or `gb_day` meter is reported by the aggregate's `components` array. The three input dimensions partition the prompt — `input_tokens` = `uncached_input_tokens` + `cached_tokens` + `cache_write_tokens` — and are separate because they are separately priced. Note that the `input_tokens` **component** on a usage event is the uncached figure alone; here `input_tokens` is the whole prompt.
    - `cost_usd` number, nullable: Sum of priced component costs; null when nothing in the slice was priced. Always null at step altitude — the ledger prices one event per generation at write time, and a price read back per step would disagree with it after any price change.
    - `input_tokens` integer: Full prompt tokens, reconstructed from the components.
    - `uncached_input_tokens` integer: Prompt tokens priced at the plain input rate — the prompt minus cache reads and cache writes.
    - `output_tokens` integer
    - `cached_tokens` integer: Prompt tokens served from the provider's prompt cache.
    - `cache_write_tokens` integer: Prompt tokens written into the provider's prompt cache.
    - `reasoning_tokens` integer: A non-billable subset of `output_tokens`, reported where the provider breaks it out.

## Other responses

- `400`: `fork_at_position` names no message in the parent conversation, or `agent_id` is unknown or belongs to another project
- `401`: Missing or invalid credentials.
- `403`: The credential is scoped to a different project, or the caller's role in the project does not carry this action.
- `404`: The resource does not exist (existence is not leaked).

---

[API](https://skmtc.dev/naturali/apis/naturali-ai-api.md) · [All operations](https://skmtc.dev/naturali/apis/naturali-ai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/naturali/apis/naturali-ai-api/revisions/e4905b78f422?raw)
