# What is `runContext` and how to use it

**Canonical doc** for the per-run correlation object in **`@x12i/activix` v5+**. Activity boundary I/O (root **`outer`** / optional **`inner`**): [activity-structure.md](./activity-structure.md). **`sessionId`** ownership and uniqueness: [session-id-usage.md](./session-id-usage.md).

## v5 runContext-only API (quick)

**Activix v5** uses **`runContext`** end to end by default. There is **no** `identity`, `identityField`, or `findRecordsByIdentity` on the default public API—no deprecated aliases. If you pass a stray top-level **`identity`** property with default config, Activix **does not** treat it as the correlation envelope (see [MIGRATION-v5.md](./MIGRATION-v5.md)).

| Area | v5 name (default) |
|------|-------------------|
| BSON / document field | **`runContext`** |
| Collection config | **`runContextField`** |
| Write payloads | **`runContext: { … }`** |
| Query helper | **`findRecordsByRunContext`** |
| Criteria | **`FindByRunContextCriteria`** / property **`runContext`** |

The word **“identity”** elsewhere (auth, cloud SDKs, **logs-gateway** call sites) is **unrelated** to this object.

---

## The one-sentence version

**`runContext` tells Activix _where this AI call sits_ in your work hierarchy — not _who_ is calling, and not _how_ Activix is configured.**

---

## The confusion (and why it happens)

People keep mixing up three things that are completely separate:

| What | Where it goes | When you set it | Example |
|------|--------------|-----------------|---------|
| **Activix config** | `new Activix({ ... })` | Once, at app startup | `mongoUri`, `collection`, `storageMode` |
| **User/caller identity** | Your auth layer, request headers, middleware | Per request, _before_ Activix | JWT claims, API keys, tenant ID |
| **Run context** | `ax.startRecord({ runContext: { ... } })` | Per call, _at write time_ | `sessionId`, `jobId`, `taskId` |

`runContext` is **only** the third one. It is the coordinate system that says: _"this particular AI invocation belongs to this job, this task, at this point in the execution chain."_

It is not identity. It is not configuration. It is a breadcrumb trail through your work hierarchy.

---

## The hierarchy: outside → inside

Think of your system as a set of nested scopes. Each scope gets narrower:

```
┌─────────────────────────────────────────────────────────┐
│  JOB                                                    │
│  sessionId + jobId + jobTypeId                          │
│                                                         │
│  ┌───────────────────────────────────────────────────┐  │
│  │  TASK                                             │  │
│  │  taskId + taskTypeId                              │  │
│  │                                                   │  │
│  │  ┌─────────────────────────────────────────────┐  │  │
│  │  │  SUPPORTING TASK / SKILL                    │  │  │
│  │  │  skillId, stepId, parentActivityId, ...     │  │  │
│  │  │                                             │  │  │
│  │  │  ┌───────────────────────────────────────┐  │  │  │
│  │  │  │  AI ACTIVITY (this Activix record)    │  │  │  │
│  │  │  │  activityId (set by Activix)          │  │  │  │
│  │  │  │  aiRequestId (your leaf call ID)      │  │  │  │
│  │  │  └───────────────────────────────────────┘  │  │  │
│  │  └─────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
```

### What each layer does and who runs it

Every level in the hierarchy answers two questions:

1. **What work is being done?** — described by the level's type and ID fields.
2. **Which instance is doing it?** — the specific executor (service, worker, agent, replica) that picked up and ran this piece of work.

The `instance` travels with `runContext` and reflects _whoever is executing at that point in the chain_. As work moves from one executor to another, `instance` changes — but the upstream IDs (`sessionId`, `jobId`, `taskId`) stay the same.

---

#### Layer 1 — Job

| Question | Answer |
|----------|--------|
| **What is it doing?** | Orchestrating the entire unit of work. A "csv-import" job, a "monthly-report" job, a "customer-onboarding" job. Identified by `jobId` and `jobTypeId`. |
| **Who is running it?** | The job scheduler or orchestrator that picked up the job. e.g. a BullMQ worker on host `worker-03`, a Lambda function, a K8s pod. That's your `instance` at this level. |
| **What does it set in `runContext`?** | `sessionId` (often same as `jobId`), `jobId`, `jobTypeId`, and `instance` reflecting the orchestrator. |

```typescript
// The job scheduler picks up a job and starts the chain
const jobRunContext = {
  sessionId: 'job-20240315-001',
  jobId: 'job-20240315-001',
  jobTypeId: 'csv-import',
  instance: { instanceId: 'worker-03', type: 'bullmq-worker' },
};
```

---

#### Layer 2 — Task

| Question | Answer |
|----------|--------|
| **What is it doing?** | A specific piece of work within the job. "Process customers.csv", "generate-summary-section", "validate-schema". Identified by `taskId` and `taskTypeId`. |
| **Who is running it?** | Might be the same worker that runs the job, or a different service/agent that the job dispatched to. If the executor changes, `instance` updates to reflect the new one. |
| **What does it set in `runContext`?** | Inherits everything from the job layer, adds `taskId` and `taskTypeId`, and updates `instance` if the executor changed. |

```typescript
// The job dispatches task 1 to a processing service
const task1RunContext = {
  ...jobRunContext,                    // inherits sessionId, jobId, jobTypeId
  taskId: 'task-cust-001',
  taskTypeId: 'column-classification',
  instance: { instanceId: 'proc-svc-12', type: 'processing-service' },
  // ↑ different executor than the job scheduler
};
```

If the same worker runs both the job and its tasks, `instance` can stay the same — it reflects reality.

---

#### Layer 3 — Supporting task / skill

| Question | Answer |
|----------|--------|
| **What is it doing?** | Finer-grained work under a task: a skill invocation, a step in a pipeline, a sub-routine. Identified by `skillId`, `stepId`, or similar product-specific fields. Not necessarily its own Activix row — depends on your granularity. |
| **Who is running it?** | Often the same executor as the task, but could be a specialized agent or a different replica. `instance` reflects whoever is doing the actual work at this point. |
| **What does it set in `runContext`?** | Inherits everything from above, adds its own scope fields (`skillId`, `stepId`, etc.), updates `instance` if the executor changed. |

```typescript
// A skill-based sub-step within the task
const skillRunContext = {
  ...task1RunContext,                  // inherits sessionId, jobId, taskId, etc.
  skillId: 'skill-nlp-classifier-v2',
  stepId: 'step-extract-headers',
  instance: { instanceId: 'agent-nlp-07', type: 'skill-agent' },
  // ↑ this hop is handled by a specialized agent
};
```

> **Field names are product-defined below job/task.** This doc uses `skillId` and `stepId` as examples, but your product may use `graphId`, `nodeId`, `masterSkillId`, `masterSkillActivityId`, or other names for the same idea — deeper scope anchors in the work tree. Add whatever IDs your system actually uses; the pattern (inherit above, extend here) is the same regardless of the field name.

---

#### Layer 4 — AI activity (the Activix record)

| Question | Answer |
|----------|--------|
| **What is it doing?** | One LLM / AI gateway invocation — the leaf of the chain. This is the call that Activix actually records. The work is described in **`outer`** (and optional **`inner`**), not in `runContext`. |
| **Who is running it?** | The service or agent that makes the actual LLM call. Might be the same executor as the layer above, might be the AI gateway itself. `instance` reflects whichever executor is doing this hop. |
| **What does it set in `runContext`?** | Inherits everything from above. Optionally adds `aiRequestId` (your own ID for this specific invocation). `activityId` is created by Activix on `startRecord` — you don't set it yourself. |

```typescript
const { activityId } = await ax.startRecord({
  runContext: {
    ...skillRunContext,                // inherits the full chain
    aiRequestId: 'req-xyz-789',        // optional: your leaf-call ID
    instance: { instanceId: 'gw-east-2', type: 'ai-gateway' },
    // ↑ the gateway instance that actually talks to the LLM
  },
  ...activixActivityIo(
    activixOuterTier(
      { kind: 'classify-columns', args: { file: 'customers.csv' } },
      null,
      { type: 'column-classification', provider: 'openai', model: 'gpt-4o' }
    )
  ),
});
```

---

### The full picture: one chain, four hops

Here's a single AI call with the complete `runContext`, showing how each layer added its scope and its executor:

```typescript
runContext: {
  // Layer 1: Job — "who started the whole thing and what kind of job is it"
  sessionId: 'job-20240315-001',
  jobId: 'job-20240315-001',
  jobTypeId: 'csv-import',

  // Layer 2: Task — "which piece of work within that job"
  taskId: 'task-cust-001',
  taskTypeId: 'column-classification',

  // Layer 3: Skill — "which sub-routine within that task"
  skillId: 'skill-nlp-classifier-v2',
  stepId: 'step-extract-headers',

  // Layer 4: Leaf call — "this specific AI invocation"
  aiRequestId: 'req-xyz-789',

  // Executor: who is running THIS hop (the innermost one)
  instance: { instanceId: 'gw-east-2', type: 'ai-gateway' },
}
```

> **Note on `instance`:** the `runContext` that gets persisted on the Activix record carries the `instance` of _the executor at the point of the write_. If you need to trace which executor ran each intermediate layer, either log that separately or record it in `outer.metadata`. The persisted `instance` answers: "who made this specific AI call?"

---

## What goes in `runContext` — full shape

```typescript
runContext: {
  // ── Job / session anchor ──
  sessionId: string,         // Required. Shared by everything in this job.
  jobId?: string,            // The job that started the chain.
  jobTypeId?: string,        // What kind of job (e.g. "data-import", "report-gen").

  // ── Task anchor ──
  taskId?: string,           // Which task within the job.
  taskTypeId?: string,       // What kind of task.

  // ── Deeper nesting (if applicable) ──
  stepId?: string,           // Step within a task.
  skillId?: string,          // If this is a skill-based chain.

  // ── Leaf call (optional) ──
  aiRequestId?: string,      // Your own ID for this specific invocation.

  // ── Executor: who is running this hop ──
  instance?: {
    instanceId: string,      // Which agent / worker / replica.
    type: string,            // e.g. "agent", "worker", "lambda", "ai-gateway".
  },
}
```

You don't need all of these. Use what your system actually has. For correlation you should still pass **`sessionId`** when you have it: if both `runContext.sessionId` and top-level `sessionId` are missing, Activix **does not** invent a value—it **warns** once per write and stores `runContext` without `sessionId`.

---

## Concrete example

Imagine a data-import job that processes CSV files. The job spawns tasks (one per file), and each task calls the AI gateway to classify columns. Watch how `runContext` grows at each hop and how `instance` changes when the executor changes.

```typescript
// ── Layer 1: The job scheduler picks up the job ──
// What: orchestrate a CSV import
// Who:  BullMQ worker on host worker-03
const jobRunContext = {
  sessionId: 'job-20240315-001',
  jobId: 'job-20240315-001',
  jobTypeId: 'csv-import',
  instance: { instanceId: 'worker-03', type: 'bullmq-worker' },
};

// ── Layer 2: Task 1 — dispatched to a processing service ──
// What: classify columns in customers.csv
// Who:  processing service replica proc-svc-12
const task1RunContext = {
  ...jobRunContext,
  taskId: 'task-cust-001',
  taskTypeId: 'column-classification',
  instance: { instanceId: 'proc-svc-12', type: 'processing-service' },
  // ↑ instance changed: different executor than the job scheduler
};

// ── Layer 4: The AI call inside task 1 ──
// What: one LLM invocation to classify the columns
// Who:  AI gateway instance gw-east-2
const { activityId } = await ax.startRecord({
  runContext: {
    ...task1RunContext,
    aiRequestId: 'req-abc-001',
    instance: { instanceId: 'gw-east-2', type: 'ai-gateway' },
    // ↑ instance changed again: the gateway makes the actual LLM call
  },
  ...activixActivityIo(
    activixOuterTier(
      { kind: 'classify-columns', args: { file: 'customers.csv' } },
      null,
      { type: 'column-classification', provider: 'openai', model: 'gpt-4o' }
    )
  ),
});

// ── Layer 2: Task 2 — same job, different task, maybe same or different executor ──
// What: classify columns in orders.csv
// Who:  same processing service, different replica
const task2RunContext = {
  ...jobRunContext,
  taskId: 'task-ord-002',
  taskTypeId: 'column-classification',
  instance: { instanceId: 'proc-svc-08', type: 'processing-service' },
};

const { activityId: id2 } = await ax.startRecord({
  runContext: {
    ...task2RunContext,
    aiRequestId: 'req-abc-002',
    instance: { instanceId: 'gw-east-2', type: 'ai-gateway' },
  },
  ...activixActivityIo(
    activixOuterTier(
      { kind: 'classify-columns', args: { file: 'orders.csv' } },
      null,
      { type: 'column-classification', provider: 'openai', model: 'gpt-4o' }
    )
  ),
});
```

Now you can query everything that happened in that job:

```typescript
// All activities for the entire job
const all = await ax.findRecordsByRunContext({
  sessionId: 'job-20240315-001',
});

// Just the ones still running
const running = await ax.findRecordsByRunContext({
  sessionId: 'job-20240315-001',
  status: 'started',
});
```

---

## Rules of thumb

### 1. Pass `runContext` on every write, not in the constructor

The constructor (`new Activix({ ... })`) configures _storage_ — where documents go, which collection, what indexes. It knows nothing about the current job or task. That context comes at call time:

```typescript
// ✅ Right: runContext travels with each write
await ax.startRecord({ runContext: { sessionId, jobId, taskId }, ... });

// ❌ Wrong: trying to bake runContext into the constructor
const ax = new Activix({ sessionId: '...', jobId: '...' }); // these are not constructor options
```

### 2. Inherit upward, extend downward — but `instance` reflects the current executor

Each layer receives the run context from its parent and adds its own scope. Never replace upstream IDs (`sessionId`, `jobId`) — extend them. But `instance` is different: it always reflects **whoever is executing right now**, so it changes when the executor changes:

```typescript
// Job layer sets the anchor
const jobCtx = {
  sessionId: 'abc', jobId: 'abc', jobTypeId: 'import',
  instance: { instanceId: 'scheduler-01', type: 'job-scheduler' },
};

// Task layer inherits IDs, but instance changes (different executor)
const taskCtx = {
  ...jobCtx,
  taskId: 'task-1', taskTypeId: 'classify',
  instance: { instanceId: 'proc-svc-12', type: 'processing-service' },
};

// AI call: instance changes again (the gateway makes the call)
const aiCtx = {
  ...taskCtx,
  aiRequestId: 'req-001',
  instance: { instanceId: 'gw-east-2', type: 'ai-gateway' },
};
```

A nested layer should **never** overwrite `sessionId` or `jobId` with a new value. If it received those from upstream, it passes them through unchanged. `instance` is the exception — it tracks who is doing the work _now_.

### 3. `sessionId` is the minimum — add more as your system grows

If your system is simple (no jobs, no tasks, just direct AI calls), `sessionId` alone is fine:

```typescript
await ax.startRecord({
  runContext: { sessionId: requestId },
  ...
});
```

As your system gets more layered, add `jobId`, `taskId`, etc. You don't need the full hierarchy on day one.

Omitting `sessionId` is allowed, but Activix **warns** and leaves it unset—use that only when you truly have no run boundary to record.

### 4. `activityId` is Activix's concern, not yours

Activix creates the `activityId` when you call `startRecord`. You don't put it in `runContext`. You use it afterward to complete, fail, or patch the record:

```typescript
const { activityId } = await ax.startRecord({ runContext, ...activityIo });

// Later...
await ax.completeRecord(activityId, { outer: { output: result } });
```

### 5. `aiRequestId` is your leaf-call ID (optional)

If you want to track the specific invocation independently from Activix's `activityId` (e.g. you assign your own request ID before calling the LLM), put it in `runContext`:

```typescript
runContext: {
  sessionId: 'job-abc',
  taskId: 'task-1',
  aiRequestId: 'req-xyz-789',  // your own ID for this specific LLM call
}
```

This is useful when multiple systems need to correlate the same call.

> **AI Gateway users:** if you integrate through `@x12i/ai-gateway`, treat `aiRequestId` as **required** on the request — the gateway uses it as the stable leaf ID for that invocation. The "optional" label above applies to Activix generically; the gateway product makes it mandatory.

---

## Common mistakes

### "I put `sessionId` in the constructor and nothing got saved"

`sessionId` is not a constructor option. The constructor configures storage. `sessionId` goes in `runContext` at write time.

### "I created a new `sessionId` in every service"

If service A calls service B and both create their own `sessionId`, you lose the ability to trace the full chain. The originating service sets `sessionId`; downstream services receive and forward it.

### "I used `runContext` to pass user identity / tenant info"

`runContext` is about _work scope_ (which job, which task, which call), not about _who_ triggered it. If you need to store tenant or user info alongside the activity, put it in `outer.metadata`:

```typescript
...activixActivityIo(
  activixOuterTier(
    { kind: 'classify', args: { ... } },
    null,
    { type: 'classification', tenant: 'acme', userId: 'user-42' }  // metadata
  )
)
```

### "I used `identity` instead of `runContext`"

In v5 there is no `identity` field. The correlation envelope is `runContext` only. If you're migrating from v4, see [MIGRATION-v5.md](./MIGRATION-v5.md).

---

## Quick reference: where does each piece of information go?

| Information | Where it goes | Why |
|------------|--------------|-----|
| Mongo URI, collection name, storage mode | `new Activix({ ... })` | Infrastructure — set once at startup |
| `sessionId`, `jobId`, `taskId` | `runContext` on `startRecord` | Work scope — different for each execution |
| LLM input, prompt, parameters | `outer.input` | What was sent to the model (activity boundary) |
| LLM output, response | `outer.output` | What came back |
| Provider, model, cost, tenant | `outer.metadata` | Descriptive info about the call |
| Internal step-level I/O (optional) | `inner[]` entries (`input`, `output`, `metadata`, optional `cost`, `startedAt`, `endedAt`) | Structured sub-activities inside the boundary |
| `activityId` | Returned by `startRecord` | Activix manages this — don't set it yourself |
| `status`, `startTime`, `endTime` | Managed by Activix | Lifecycle fields — don't set these yourself |

---

## v5 note

The correlation object is **`runContext`** only. There is no `identity` field, no `identityField` config, and no `findRecordsByIdentity` in the public API. If you see `identity` in old code, it needs to be migrated to `runContext`. See [MIGRATION-v5.md](./MIGRATION-v5.md) and [COMMUNICATING-RUNCONTEXT-V5.md](./COMMUNICATING-RUNCONTEXT-V5.md) for details.

The default BSON key stored in MongoDB is `runContext`. Override with `runContextField` on the collection config only if you know you need a custom key (e.g. an existing collection already uses a different field name).

---

## Appendix: graphs, hooks, and extra correlation fields

**Naming:** in-task work is **not** Activix’s document primary key **`activityId`**. Prefer **`stepId`**, **`taskStepId`**, or **`invocationId`** for steps inside a task.

**Pre/post task hooks:** record which action ran with ids like **`preTaskActionId`** / **`postTaskActionId`**, or a **`taskPhase`** (`"pre"` / `"main"` / `"post"`) plus **`taskHookId`**—pick one style product-wide.

**Graphs:** add **`graphId`**, **`nodeId`**, and the chosen branch (**`graphEdgeId`**, **`edgeId`**, **`transitionId`**, or **`chosenEdgeId`**) when the runtime takes an edge.

**Propagation:** each layer **receives** `runContext` from upstream, **adds** fields for its scope, and **passes** the richer object down. Do **not** remove or replace upstream ids (`sessionId`, `jobId`, `taskId`, …). For responses, exposing the enriched `runContext` (e.g. `response.metadata.runContext`) keeps one correlation chain.

**Gateway-style normalization (illustrative):** an entry layer may align `runContext.sessionId` with a job id, add work ids (`jobTypeId`, `taskTypeId`, hooks, graph fields), and set **`instance`** from `agentId` / `agentType` before `startRecord`. Put gateway-facing labels in **`outer.metadata`** as needed.

Activix does **not** validate every optional field; this file is **guidance** when you want strong cross-service correlation.
