# Activity I/O: `outer` and `inner[]` (v6)

Each activity document stores boundary I/O at the **root** of the row—**no `structure` wrapper**:

- **`outer`** (required): activity-level **`input`**, **`output`**, **`metadata`**, optional **`cost`**.
- **`inner`** (optional): array of sub-activities. Each entry uses **`input`**, **`output`**, **`metadata`**, optional **`cost`**, and per-step timing.

**`runContext`** is separate: the correlation envelope (see [run-context-object.md](./run-context-object.md)).

**`metadata`** (optional, top-level) is separate from **`outer.metadata`**: run-level orchestrator observability (e.g. `narrixMode`, `executionStrategiesSummary`, synthesis flags). It is a sibling of **`outer`**, **`inner`**, and **`runContext`**. Query with dotted paths such as `{ "metadata.narrixMode": "handler" }`. On lifecycle updates, Activix shallow-merges top-level **`metadata`** and never hoists it into **`outer`**. Use **`outer.metadata`** only for IO-tier annotations (phase, kind, truncated diagnostics).

For studio report joins and directory export, see **[activity-trace-schema.md](./activity-trace-schema.md)** (`trace.activityKind`, `trace.activityType`, `trace.observability`, graph ↔ ai-tasks join keys).

## What integrators supply

- **`runContext`** — where in the run (`sessionId` and accumulating work ids). Receive from upstream, extend, do not replace upstream ids.
- **`outer`** — required; what crossed the activity boundary from the caller’s perspective.
- **`inner`** — optional; finer-grained internal process steps inside that boundary.

## Phased writes (typical order)

Many integrations **do not** have the full picture on `startRecord`. Activix is built for this pattern:

1. **First** you usually persist **`outer.input`** (and **`outer.metadata`**) with **`outer.output`** still unknown — you may **omit** the **`output`** key on that write; Activix normalizes it to **`null`** before validation and persistence.
2. **Either** in the same `startRecord` **or** on a later **`patchRecord` / `markInProgress`**, you add **`inner[]`** entries with **`input`** and **`startedAt`** while output is still pending.
3. **Later**, a **`completeRecord`** (or **`patchRecord`**) updates matching **`inner[]`** entries with **`output`**, **`endedAt`**, and optional **`cost`**, and/or sets **`outer.output`**.

So: **input** usually arrives first; **output** usually arrives later. For `inner[]`, include `stepId` when possible so updates replace the same step instead of appending duplicates.

## What integrators must not supply

Activix owns these; they are **stripped** from incoming payloads and set by the library:

- Primary key (`activityId` / configured `primaryKey`)
- **`status`**, **`startTime`**, **`endTime`**, **`duration`**, optional **`progressAt`**, purge / tombstone field
- **`createdAt`**, **`updatedAt`** (Activix lifecycle uses **Unix ms numbers** on configured time fields)
- **`error`** on user updates (set only by `failRecord`)

## Shape: `outer` (required)

Plain object with **exact keys**:

| Key | Requirement |
|-----|---------------|
| **`input`** | Required. Any JSON-compatible value (prompt envelope, job args, etc.). |
| **`output`** | Required **after normalization** (may be **`null`** until the activity completes). You may omit the key on an early write; it becomes **`null`**. |
| **`metadata`** | Required plain object (use `{}` if empty). Domain: **`model`**, **`type`**, **`provider`**, **`cost`**, tenant, etc. |
| **`cost`** | Optional plain object. Prefer stable keys (`usd`, `tokens`, `provider`, `model`) and put provider-specific details under `details`. |

### Recommended `outer.cost` / `inner[].cost`

Optional and validated when present:

```ts
{
  usd?: number;
  tokens?: { input?: number; output?: number; reasoning?: number; total?: number };
  provider?: string;
  model?: string;
  unit?: string;
  details?: Record<string, unknown>;
}
```

Use **`tokens.reasoning`** when the provider reports internal reasoning tokens separately from visible completion output (helps explain cases where completion totals exceed a configured output cap).

## Shape: `inner` (optional array)

When present, `inner` is an array of plain objects. Each entry should include:

- `input` (required)
- `output` (normalized to `null` when omitted on partial writes)
- `metadata` (required object)
- `startedAt` (required ISO timestamp string)
- `endedAt` (required key: ISO timestamp string or `null`)
- `durationMs` (optional; auto-derived when omitted and both timestamps exist)
- optional `stepId`, `stepName`, `status`, `cost`

Omit `inner` when you do not need internal-step tracing.

## Helpers (package exports)

- **`activixOuterTier(input, output, metadata?)`** — builds one **`outer`** object.
- **`activixInnerTier(input, output, metadata?, options?)`** — builds one `inner[]` step.
- **`activixActivityIo(outer, inner?)`** — builds **`{ outer }`** or **`{ outer, inner }`** (`inner` is an array) to spread into `startRecord`.
- **`activixTier`** — deprecated alias for **`activixOuterTier`**.
- **`activixStructure`** — deprecated alias for **`activixActivityIo`**.
- **`validateActivityStructure(doc)`** — same checks as write paths; mutates the doc to normalize missing `output` / `endedAt`, metadata objects, and derive `durationMs`.
- **`mergeActivixActivityIo`**, **`mergeOuterTier`**, **`mergeInnerTier`** — deep-merge partial **`outer`** / **`inner`**; merged tiers are passed through **`ensureOuterTierShape`** / **`ensureInnerTierShape`**.
- **`ensureOuterTierShape`**, **`ensureInnerTierShape`** — add missing fields for incremental payloads (`output`, `endedAt`, `metadata`) and derive `durationMs`.
- **`applyActivixAutoCost`**, **`needsActivixCostCalculation`**, **`findValidActivixCostInRecord`** — optional automatic **`outer.cost`** (valid cost on the row first, then **`@x12i/ai-tools`**). Enable on **`new Activix({ autoCost: true })`**. Model strings follow ai-tools v5.1 / ai-profiles 4.4.x: **`profile/choice`** or concrete ids only.

## Time fields

Activix writes **`startTime`**, **`endTime`**, and **`duration`** as **numbers** (Unix ms for start/end; ms delta for duration).

## Partial updates

`completeRecord`, `failRecord`, `markInProgress`, and `patchRecord` deep-merge `outer` and merge `inner[]` steps. Matching happens by `stepId` when present; otherwise new entries append.

- `{ inner: [{ stepId: 'fetch', output: body, endedAt: new Date().toISOString() }] }`
- `{ outer: { output: result } }`
- `{ inner: [{ stepId: 'fetch', output: body }], outer: { output: result } }`

## Legacy: nested `structure`

Older rows may still use **`structure: { outer, inner? }`**. Diagnostic helpers and playground can read **`outer`** from the root or from **`structure.outer`** for summaries only. New writes should use **root** **`outer`** / **`inner`**. See [MIGRATION-v6.md](./MIGRATION-v6.md).

## Legacy: `fullRequest` / `fullOutput`

Playground sidecars may still use **`fullRequest`** / **`fullOutput`**. New code should prefer **`outer.input`** / **`outer.output`** (and `inner[].input` / `inner[].output` when used).

## Naming note

The word **“outer”** in [run-context-object.md](./run-context-object.md) sometimes refers to **run scope** for **`runContext`**. **`outer`** **on the document** means the activity’s boundary **input** / **output** / **metadata**—not the same as run-context “outer scope.”
