# FocusSessions

> The FocusSessions operations of the Hub REST API (12 operations).

URL: https://docs.synap.live/docs/reference/hub-api/focus-sessions · Generated from Synap-core/backend@c31e261ab (main)

Paths are relative to `/api/hub` on a pod.

## GET /focus-sessions

List focus sessions for a workspace or a project

Scope by `workspaceId` (membership-checked), `projectId` (the project must be visible to the caller — `loadVisibleProject`, the same floor every project read and filing write uses), or both. At least one is required. Rows are always floored at the sessions the caller can read.

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `workspaceId` | query | string |  |  |
| `projectId` | query | string |  |  |
| `status` | query | "active" \| "paused" \| "forming" \| "scheduled" \| "closed" \| "failed" \| "cancelled" \| "stale" \| "all" |  |  |
| `lens` | query | "default" \| "triage" \| "all" |  |  |
| `kind` | query | "work" \| "run" \| "receipt" \| "all" |  |  |
| `playbookId` | query | string |  |  |
| `automationId` | query | string |  |  |
| `subjectEntityId` | query | string |  |  |
| `subjectProfileSlug` | query | string |  |  |
| `limit` | query | integer |  |  |

Responses:

- `200`: Sessions
- `400`: Bad request
- `403`: Forbidden
- `404`: Project not found
- `500`: Internal error

## POST /focus-sessions

Create a focus session

IS creates a focus session, optionally with a correlationId for idempotency. If a session with the same correlationId already exists it is returned as-is.

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | string |  |  |
| `projectId` | string |  |  |
| `trackId` | string |  |  |
| `trackStage` | string |  |  |
| `userId` | string | yes |  |
| `title` | string |  |  |
| `goal` | string | yes |  |
| `correlationId` | string |  |  |
| `templateId` | string \| null |  |  |
| `params` | object |  |  |
| `outcomes` | array<object> |  |  |
| `expectedOutputs` | array<object> |  |  |
| `criteria` | array<object> |  |  |
| `channelId` | string |  |  |
| `agentIds` | array<string> |  |  |
| `subjectEntityId` | string |  |  |
| `parentSessionId` | string |  |  |
| `suspendedIntent` | string |  |  |
| `blockedBySessionIds` | array<string> |  |  |
| `forceCreate` | boolean |  |  |

Responses:

- `200`: Created or existing session
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error

## GET /focus-sessions/:id

Get a focus session by ID

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |
| `workspaceId` | query | string |  |  |

Responses:

- `200`: Session
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error

## PATCH /focus-sessions/:id

Update a focus session

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `workspaceId` | string |  |  |
| `status` | "active" \| "paused" \| "forming" \| "scheduled" \| "closed" \| "failed" \| "cancelled" |  |  |
| `progress` | integer |  |  |
| `channelId` | string |  |  |
| `correlationId` | string |  |  |
| `title` | string \| null |  |  |
| `goal` | string |  |  |
| `agentIds` | array<string> |  |  |
| `addAgentId` | string |  |  |
| `outcomes` | array<object> |  |  |
| `expectedOutputs` | array<object> |  |  |
| `verificationReport` | object |  |  |
| `currentStage` | string |  |  |
| `subjectEntityId` | string \| null |  |  |
| `metadata` | object |  |  |
| `criteria` | array<object> |  |  |
| `followPlaybookId` | string \| null |  |  |
| `followStageKey` | string \| null |  |  |
| `params` | object |  |  |
| `trackId` | string \| null |  |  |
| `trackStage` | string \| null |  |  |
| `agentUserId` | string |  |  |
| `reasoning` | string |  |  |

Responses:

- `200`: Updated session
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error

## POST /focus-sessions/:id/complete

Complete (close) a focus session and return the proposal pack

Lifecycle close via completeFocusSession — stamps closed, finishes any running playbook_run, returns pendingProposals + counts + warnings. Distinct from POST .../complete-run (playbook run only).

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `summary` | string |  |  |
| `verificationReport` | object |  |  |
| `terminalStatus` | "closed" \| "cancelled" \| "failed" |  |  |

Responses:

- `200`: Closed session + proposal pack
- `400`: Bad request
- `403`: Forbidden or proposed
- `404`: Not found
- `500`: Internal error

## POST /focus-sessions/:id/outputs

Record an existing object as a session output

Writes one `artifacts` provenance row attributing an already-existing view/cell/document/entity/url to this session. `expectedLabel` claims a declared deliverable slot for the join; it never stamps it done.

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `kind` | "view" \| "cell" \| "document" \| "entity" \| "url" \| "automation" \| "playbook" | yes |  |
| `refId` | string | yes |  |
| `label` | string |  |  |
| `expectedLabel` | string |  |  |
| `workspaceId` | string |  |  |

Responses:

- `200`: Recorded
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error

## POST /focus-sessions/:id/outputs/delegate

Hand a declared deliverable to an agent

Posts one ask in the session room, starts an agent turn for it, and stamps `delegatedTo`/`delegatedAt` on the named slot. Never stamps `status` — only an approval may do that.

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `expectedLabel` | string | yes |  |
| `agentType` | string |  |  |

Responses:

- `200`: Delegated
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error

## POST /focus-sessions/:id/outputs/block

Hand a declared deliverable to the human

Stamps `owner: 'human'` + `blockedReason` + `why` + `owedSince` on the named slot. The slot stays `pending` — declaring that you cannot do the work is the opposite of having done it. When a standing guideline covers this kind of block, `blockGuidelines` carries its text.

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `expectedLabel` | string | yes |  |
| `blockedReason` | "credential" \| "permission" \| "capability" \| "policy" \| "decision" \| "physical" | yes |  |
| `why` | string |  |  |
| `ref` | object \| object \| null |  |  |
| `ask` | object \| object \| object \| object \| object \| null |  |  |

Responses:

- `200`: Blocked on the human
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error

## POST /focus-sessions/:id/outputs/unblock

Reclaim a deliverable handed to the human

Clears `owner`, `blockedReason`, `why` and `owedSince` together. The deliverable itself is unchanged and still owed.

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `expectedLabel` | string | yes |  |

Responses:

- `200`: Reclaimed
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error

## POST /focus-sessions/:id/outputs/answer

Answer what an agent asked about a deliverable (people only)

Records the person's answer on the named slot and posts it into the session room as the person. Not a delivery: `status` is untouched. A slot that was handed to the person goes back to the agent with the answer attached, and a pod-run agent that asked is woken. Emits `focus_session.slot_answered.completed`. Refused (403) for an agent key. A slot with a typed `ask` takes `value` (validated against it; 400 `ask_invalid:` when it does not fit, 409 `ask_changed:` when `askFingerprint` no longer matches the slot's ask).

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Request body (`application/json`): object

| Field | Type | Required | Description |
|---|---|---|---|
| `expectedLabel` | string | yes |  |
| `text` | string |  |  |
| `value` | object \| object \| object \| object \| object |  |  |
| `askFingerprint` | string |  |  |
| `expectedGranteeId` | string |  |  |

Responses:

- `200`: Answered
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `409`: The slot's ask changed (`ask_changed:`)
- `500`: Internal error

## GET /focus-sessions/:id/answers/wait

Wait (bounded long-poll) for the session owner's next answer

Returns as soon as an answer newer than `since` exists (`status: 'answered'`, the same page shape as GET /answers), or after `timeoutSeconds` (default 50, max 90) with `status: 'timeout'` and `nextSince` to wait on again. Without `since`, only UNREAD answers: slot answers not yet picked up, and room answers newer than the call. An agent key's read stamps the slot answer's `answerPickedUpAt` receipt.

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |
| `since` | query | string |  |  |
| `timeoutSeconds` | query | integer |  |  |
| `limit` | query | integer |  |  |

Responses:

- `200`: Answered, or timed out
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error

## GET /focus-sessions/:id/answers

Poll the session owner's answers since a cursor

Answers the owner gave to agent questions and owed slots, `answeredAt > since`, oldest first, id-stable. Pass `nextSince` back as `since`. For agents the pod cannot wake (Claude Code, claude.ai, Raycast).

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |
| `since` | query | string |  |  |
| `limit` | query | integer |  |  |

Responses:

- `200`: A page of answers
- `400`: Bad request
- `403`: Forbidden
- `404`: Not found
- `500`: Internal error
