# @exellix/graph-engine

> Graph executor SDK

Latest version **9.8.1** (published 2026-07-11) · exellix-license license · 0 weekly downloads

## Install

```sh
npm install @exellix/graph-engine
pnpm add @exellix/graph-engine
yarn add @exellix/graph-engine
bun add @exellix/graph-engine
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 9.8.1 |
| Published | 2026-07-11 |
| First published | 2026-05-26 |
| Weekly downloads | 0 |
| License | exellix-license |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 26 |
| Unpacked size | 1.1 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | exellix |
| Maintainers | exellix |
| Keywords | exellix, graph, execution, workflow |

## Links

- npm: https://www.npmjs.com/package/@exellix/graph-engine
- Repository: https://github.com/exellix/exellix-engine-mono-repo
- Homepage: https://github.com/exellix/exellix-engine-mono-repo#readme
- Issues: https://github.com/exellix/exellix-engine-mono-repo/issues
- npm.io page: https://npm.io/package/@exellix/graph-engine

## Dependencies (26)

- [@x12i/env](https://npm.io/package/@x12i/env.md) ^4.0.3
- [@x12i/runx](https://npm.io/package/@x12i/runx.md) ^2.0.0
- [@x12i/funcx](https://npm.io/package/@x12i/funcx.md) ^4.10.0
- [@x12i/logxer](https://npm.io/package/@x12i/logxer.md) ^5.1.0
- [@x12i/activix](https://npm.io/package/@x12i/activix.md) ^9.0.5
- [@x12i/catalox](https://npm.io/package/@x12i/catalox.md) ^6.0.0
- [@x12i/rendrix](https://npm.io/package/@x12i/rendrix.md) ^4.3.0
- [@x12i/graphenix](https://npm.io/package/@x12i/graphenix.md) ^2.17.0
- [@exellix/ai-tasks](https://npm.io/package/@exellix/ai-tasks.md) ^10.15.16
- [@x12i/ai-profiles](https://npm.io/package/@x12i/ai-profiles.md) ^4.3.0
- [@exellix/ai-skills](https://npm.io/package/@exellix/ai-skills.md) ^7.0.20
- [@x12i/graphenix-core](https://npm.io/package/@x12i/graphenix-core.md) ^2.17.0
- [@x12i/memorix-writer](https://npm.io/package/@x12i/memorix-writer.md) ^1.33.0
- [@x12i/graphenix-logic](https://npm.io/package/@x12i/graphenix-logic.md) ^2.17.0
- [@x12i/graphenix-format](https://npm.io/package/@x12i/graphenix-format.md) ^2.0.0
- [@x12i/memorix-retrieval](https://npm.io/package/@x12i/memorix-retrieval.md) ^1.33.0
- [@x12i/memorix-descriptors](https://npm.io/package/@x12i/memorix-descriptors.md) ^1.32.0
- [@x12i/graphenix-plan-format](https://npm.io/package/@x12i/graphenix-plan-format.md) ^2.17.0
- [@x12i/graphenix-trace-format](https://npm.io/package/@x12i/graphenix-trace-format.md) ^2.17.0
- [@x12i/graphenix-plan-compiler](https://npm.io/package/@x12i/graphenix-plan-compiler.md) ^2.17.0
- [@x12i/graphenix-authoring-format](https://npm.io/package/@x12i/graphenix-authoring-format.md) ^2.17.0
- [@x12i/graphenix-execute-envelope](https://npm.io/package/@x12i/graphenix-execute-envelope.md) ^2.17.0
- [@x12i/graphenix-task-node-format](https://npm.io/package/@x12i/graphenix-task-node-format.md) ^2.17.0
- [@x12i/graphenix-executable-format](https://npm.io/package/@x12i/graphenix-executable-format.md) ^2.17.0
- [@x12i/graphenix-authoring-analysis](https://npm.io/package/@x12i/graphenix-authoring-analysis.md) ^2.17.0
- [@x12i/graphenix-executable-contracts](https://npm.io/package/@x12i/graphenix-executable-contracts.md) ^2.17.0

## Alternatives

- [apollo-link-http-common](https://npm.io/package/apollo-link-http-common.md) — 879.0K weekly downloads
- [react-relay](https://npm.io/package/react-relay.md) — 336.8K weekly downloads
- [relay-test-utils](https://npm.io/package/relay-test-utils.md) — 181.6K weekly downloads
- [@vendure/core](https://npm.io/package/@vendure/core.md) — 14.8K weekly downloads
- [@pnpm/deps.graph-sequencer](https://npm.io/package/@pnpm/deps.graph-sequencer.md) — 13.4K weekly downloads

## Recent versions

- 9.8.1 (latest) — 2026-07-11
- 9.8.0 — 2026-07-11
- 9.7.0 — 2026-07-11
- 9.6.0 — 2026-07-10
- 9.5.17 — 2026-07-07
- 9.5.16 — 2026-07-05
- 9.5.15 — 2026-07-05
- 9.5.14 — 2026-07-05
- 9.5.13 — 2026-07-05
- 9.5.12 — 2026-07-05
- 9.5.11 — 2026-07-05
- 9.5.10 — 2026-06-30
- 9.5.9 — 2026-06-29
- 9.5.7 — 2026-06-29
- 9.5.6 — 2026-06-29
- … 76 more at https://npm.io/package/@exellix/graph-engine/versions

## README

# `@exellix/graph-engine` — Clean Graph Executor SDK

A minimal, focused SDK for executing graphs in the exellix ecosystem.

## What this package does and does not

**In scope — what this package does:** On each **`createExellixGraphRuntime(...).executeGraph({ plan, runtime })`** invocation, it runs **exactly one graph run** (validate plan → schedule waves → nodes → finalizer) until that run **completes or fails**, then returns the result. There is **no** batching, queueing, or multi-job orchestration inside this package — each call is one logical run. You supply a compiled v2 **`plan: ExecutableGraphPlanV2`** (from `@x12i/graphenix-plan-compiler` or {@link compileExellixExecutablePlan}), planner (`GraphEngineFactory`), tasks client, and dynamic **`runtime: GraphRuntimeObject`** with the mandatory host `jobId`, `job` envelope, active `input`, memory, variables, and per-run options. The engine generates a fresh **`taskId`** (UUID) per invocation and sends it on **every** `runTask` request together with **`jobId`**. That is the **entire** product role of graph-engine.

**Out of scope — what this package does not do:** It does **not** schedule work, own **execution matrices**, manage **claims** or **rows**, persist **job** lifecycle, implement **retry/requeue policy**, or **track** runs across tenants or sessions. Optional helpers and docs for matrix *hosts* only help **build arguments** for the same single-run API; they do **not** move orchestration into this package. Integrations (e.g. Activix graph-run events) emit data **for that call** when you wire an `eventEmitter` — they do not make graph-engine a workflow or matrix service.

## Core Responsibilities

`exellix-graph-engine` does exactly this loop:

1. **Validate executable plan:** The caller supplies **`plan: ExecutableGraphPlanV2`** (compiled upstream). The effective correlation **`graphId`** on the result and in telemetry is **`plan.source.graphId`**.
2. `executeGraph({ plan, runtime })`
3. For each runnable node:
   - Map node → `skillKey` (strict rules)
   - If `skillKey` is a **local skill** (`scoped-data-reader`, `deterministic-rule`, `scoped-answer-writer`, `scoped-answer-assembler`), run it in-process (no `ai-tasks` call); otherwise call `ai-tasks.runTask(...)`
   - Commit output into graphenix
4. Repeat until done/fail

**Nothing else** beyond orchestration, memory mapping, finalizers, and the small local-skill surface above — and **not** matrix/worker/queue ownership (see [above](#what-this-package-does-and-does-not)).

### Specification & documentation

| Topic | Document |
|--------|----------|
| **Object contracts** (model × runtime + per-call resolution view) | Index: [`formats-documentations/README.md`](formats-documentations/README.md) — [`graph-model`](formats-documentations/graph-model-object-format.md), [`graph-runtime`](formats-documentations/graph-runtime-object-format.md), [`task-node-model`](formats-documentations/task-node-model-object-format.md), [`task-node-runtime`](formats-documentations/task-node-runtime-object-format.md), [`ExellixRuntimeObject`](formats-documentations/exellix-runtime-object-format.md) |
| **Executable graph JSON** (top-level shape, task/finalizer nodes, edges, variables, `metadata.graphExecution`, canonical root enforcement, execution memory, local skills, Narrix / web scope (**forwarded `narrix` → `runTask`**), **graph JSON vs outbound `runTask`**) | **[`.docs/exellix-graph-engine-format.md`](.docs/exellix-graph-engine-format.md)** — start here for authors and schema tooling |
| Task node bridge (shell, `metadata`, `executionPipeline`, `aiTaskProfile` → Narrix web merge, composer alignment) | [`.docs/task-node-exellix-graph-engine-and-graph-composer.md`](.docs/task-node-exellix-graph-engine-and-graph-composer.md) |
| Layer 01 / 08 graph entry & response contracts | [`.docs/graph-io-visibility.md`](.docs/graph-io-visibility.md) |
| Graph entry `dataFilters` v1 / public evaluator | [`.docs/data-filters-evaluation.md`](.docs/data-filters-evaluation.md) |
| Task-node `conditions` + conditional `modelConfig.cases` (runx) | [`.docs/task-node-conditions-evaluation.md`](.docs/task-node-conditions-evaluation.md) |
| **Model profile aliases** (7.x+: graph = aliases / `profile/choice`; ai-tasks resolves per phase) | [`BREAKING-CHANGES.md`](BREAKING-CHANGES.md) §7.6 / §7.7, [`.docs/ai-tasks-model-profile-aliases-7x.md`](.docs/ai-tasks-model-profile-aliases-7x.md), [`.docs/graph-engine-ai-tasks-boundary.md`](.docs/graph-engine-ai-tasks-boundary.md) |
| Platform vs implementation (no domain operators in schema) | [`.docs/platform-generic-vs-implementation.md`](.docs/platform-generic-vs-implementation.md) |
| Activix records, `runContext`, collection tracking (`@x12i/activix` 8.4+) | [`.docs/activix-records.md`](.docs/activix-records.md) |
| Bundled graph examples & bundle README | [`graphs/README.md`](graphs/README.md) |

The **`runTask`** wire contract (identity, canonical **`input`**, optional extra ai-tasks PRE/POST utility calls vs `executionPipeline`) is summarized later under **Run identity** and **`runTask` request contract**, and expanded in the graph format doc’s [**Graph JSON vs outbound `runTask`**](.docs/exellix-graph-engine-format.md#graph-json-vs-outbound-runtask) section. Graph-engine integrates with **`@exellix/ai-tasks`** at the semver range declared in **`package.json`** (currently **`^8.6.8`**); use your **lockfile** as the tested line. Variable buckets align with ai-tasks **≥ 7.6.2** (two-scope passthrough). There is **no** minimum graph-engine ↔ ai-tasks matrix published inside ai-tasks — follow dependency semver and upstream **`CHANGELOG.md`**. Graph-engine does **not** import **`@exellix/xynthesis`** directly; **`@x12i/funcx`** is used only for optional **runx** (conditional edges / `modelConfig.cases`), not for ai-tasks execution strategies (those run via xynthesis inside ai-tasks **8.5+**).

### Canonical executable graph document (strict boundary)

An executable graph model object may have **only** these **top-level** keys: `id`, `version`, `modelConfig`, `jobKnowledge`, `nodes`, `edges`, `variables`, `response`, `metadata`. The required root **`response`** is the single executable final response contract used to build `ExecuteGraphResult.finalOutput`. Document-model and authoring fields (`name`, `description`, `exellixContractTarget`, `graphExecution`, `graphEntry`, `catalogRequests`, and similar) belong **under `metadata`** only. Node-scoped `taskKnowledge` belongs under each task node, not at the model root. Runtime state (`input`, `jobMemory`, `taskMemory`, `executionMemory`, `outputsMemory`, per-run options) belongs under the execution request’s **`runtime`** object and is rejected on the model. **`runtime.modelConfig`**, **`runtime.aliasConfig`**, and **`runtime.nodes[id].modelConfig`** were removed in **7.7** — model profiles belong on the graph document only.

`metadata.graphExecution` can document graph execution defaults and labels, including `mode: 'forward' | 'backward' | 'hybrid'`, optional `goalNodeId`, optional `dimension`, `outputMode: 'mappedAggregation' | 'lastExitNode'`, `coreObjective`, optional `nodesResponses`, and metadata-only `flowOutline: 'linearSequence' | 'convergingParallelFlow'`. Planner mode comes from `plan.schedulingPolicy.mode` or `runtime.mode`; `outputMode` does not decide the returned `finalOutput`.

Enforcement: `executeGraph`, `createExellixGraphRuntime().executeGraph`, `executeNode` when a graph is passed in context, `inspectGraph`, `inspectGraphContracts`, `validateCatalogPlanning`, and Catalox graph validators call this check. Failures throw `ExellixGraphError` with code `NON_CANONICAL_GRAPH_DOCUMENT`. For CI or custom loaders, call **`assertCanonicalGraphDocument`** (exported from the package root) on parsed JSON before execution. **`loadGraph`** returns whatever the loader parsed; validation runs when you execute or inspect, not necessarily on load.

## Installation

```bash
npm install @exellix/graph-engine
```

**Upstream tasks SDK:** This package depends on **`@exellix/ai-tasks` ^8.6.8** and **`@x12i/funcx` 4.4.4** (see **`package.json`**). Graph-engine emits **`RunTaskRequest`** shapes that match the ai-tasks **8.x** closed schema (mandatory **`executionStrategies`**, three-slot **`modelConfig`**, **`xynthesized`**, optional **`smartInput`**, no legacy root mirrors). Pin compatible versions in your app lockfile. Optional CI improvement: fail or warn when the resolved ai-tasks major/minor drifts outside an allowlist (not enforced in-repo today).

### Execution matrix hosts (`@x12i/exellix-runtime`) — documentation only for the engine

Matrix **claim**, **rows**, and **retry policy** live outside this package. If your host wires **matrix → graph run**, see [.docs/execution-matrix-handoff.md](.docs/execution-matrix-handoff.md) for how **you** should inject `runtime.executeGraph`, compile **`GraphModelObject` → plan**, resolve **`metadata.graphEntry` per model**, seed **`runtime.executionMemory`**, and pass **`runtime.jobId`**. Helpers such as **`buildMatrixJobForGraphRun`** align **`id`** and **`job.jobId`** on the `job` object so you can call **`runtime.executeGraph({ plan, runtime: { jobId: job.jobId, job, … } })`**. Those exports are **optional helpers for callers**; they do **not** expand graph-engine’s role beyond **executing the single run** when invoked.

### Configuration (`.env`)

- **Template:** [`.env.example`](.env.example) — copy to `.env` at the project root for local tests and scripts that load `dotenv`.
- **Semantics:** [`.docs/environment-and-xmemory-databases.md`](.docs/environment-and-xmemory-databases.md) (must-have vs nice-to-have, Mongo / xmemory / Narrix / Activix).
- **Resolvable path after install:** subpath export `@exellix/graph-engine/env.example` points at the same file (e.g. `require.resolve('@exellix/graph-engine/env.example')` in Node).

### Package entrypoints

- **`@exellix/graph-engine`** — Graph executor, types, loaders, integrations (platform).
- **`@exellix/graph-engine/testkit`** — Harness helpers (`InMemoryGraphLoader`, `DepGraphEngineFactory`, `RealTasksClient`) and **sample** `registerNarrixGraphTasks` for repo graphs that still call `narrix/load-input`, `narrix/to-cni`, etc. This is not re-exported from the root entry.

### Catalox and graph planning catalog IDs

Skill templates and Catalox wiring for packaged runs live in **`@exellix/ai-tasks`** (and its upstream stack). This package only offers an optional **planning-descriptor** check when you already have a Catalox client:

- **`validateGraphPlanningCatalogDescriptorsInCatalox(graph, catalox, ctx)`** — For every `catalogId` collected from `metadata.catalogBinding` / planning metadata (see [`getGraphCatalogs`](src/inspection/graphInspection.ts)), asserts `catalox.getCatalogDescriptor(ctx, catalogId)` is non-null.

For Firebase Admin, `createCataloxFromEnv`, `listAiSkillsCatalogItems`, and related helpers, import from **`@exellix/ai-tasks`** or the graph-engine re-exports below instead of adding `@exellix/ai-skills` here.

**`taskConfiguration.aiTasksOutputValidation`** (shape: `{ schema, mode?, validateWhenMissing? }`) is forwarded on `runTask` as `outputValidation` for server-side checks in `@exellix/ai-tasks`. The top-level node field **`outputValidation`** with **`rules`** remains a **local** post-check in `executeNode` only.

### Task-node preflight (validation & analysis, no `runTask`)

Graph-engine exposes the **`@exellix/ai-tasks`** preflight surface so studios and matrix hosts can validate a node **before** execution without a second dependency on ai-tasks:

| Export | Purpose |
|--------|---------|
| **`buildTaskNodeRunTaskRequest`** | Build the same outbound `RunTaskRequest` as `executeNode` (skips local skills and finalizers). |
| **`validateTaskNodeRunTaskConfig`** | Static config checks (`agentId`, pipeline, `smartInput`, `llmCall`, …). |
| **`validateTaskNodeRunTaskInvoke`** | Config + payload path resolution + optional template/smart-input render checks. |

Lower-level helpers (`validateRunTaskConfig`, `validateRunTaskInvoke`, `analyzeExpectedRunTaskInput`, Rendrix `listTokens` / `analyzeTemplateResolution`, …) are **re-exported** from the package root when you already have a `RunTaskRequest`. Catalox-backed skill invoke packet analysis was removed from `@exellix/ai-tasks` **8.6** — use **`@exellix/graph-composer`** for studio / skills-manager invoke preflight (see `@exellix/ai-tasks` `documenations/studio-skill-invoke-preflight.md`).

**Testing safety:** Default `npm test` uses **mocked** Catalox in catalog validation tests and does not open Firestore. Do **not** point `FIRESTORE_LIVE_TESTS` / integration flags at a **production** Firebase project; Catalox’s own docs recommend a dedicated test project for live integration runs.

## Quick Start

```typescript
import { compileExellixExecutablePlan, createExellixGraphRuntime } from '@exellix/graph-engine';

const runtime = createExellixGraphRuntime({
  graphLoader: myGraphLoader,
  engineFactory: myEngineFactory, // GraphEngineFactory (e.g. DepGraphEngineFactory)
  tasksClient: myTasksClient,     // TasksClientLike — responses may use `ok` or `success` (both accepted)
});

// graphModel carries static modelConfig (profile aliases on the graph document — not on runtime).
const graphModel = {
  id: 'my-graph',
  modelConfig: {
    cases: [{
      modelConfig: {
        preActionModel: 'cheap',
        skillModel: 'balanced',
        postActionModel: 'cheap',
      },
    }],
  },
  nodes: [/* … */],
  // …
};

const graphRuntime = {
  jobId: 'job-123',
  job: { agentId: 'agent-1', input: {} },
  input: { question: 'Analyze this record' },
};

const plan = compileExellixExecutablePlan(graphModel, graphRuntime);

// Host correlation id (required). Engine sets `job.id` / `job.jobId` from it and generates `result.taskId`.
const result = await runtime.executeGraph({
  plan,
  runtime: graphRuntime,
});

// Canonical business output + per-run ids:
console.log(result.finalOutput, result.jobId, result.taskId);
```

## Public API

### `runtime.executeGraph({ plan, runtime })`

Execute a complete graph through the **single canonical client API**: `createExellixGraphRuntime(...)`. The runtime owns local-skill interception, conditional edge filtering, optional `eventEmitter`, optional `debugMode`, and produces one `ExecuteGraphResult` shape.

Every call requires a compiled v2 **`plan: ExecutableGraphPlanV2`** and a dynamic **`runtime: GraphRuntimeObject`**. Hosts compile authoring graphs with `@x12i/graphenix-plan-compiler` (or {@link compileExellixExecutablePlan} for legacy exellix `GraphModelObject`) before invoke. `runtime.jobId` is mandatory and non-empty. The engine also generates a **`taskId`** (UUID) per invocation. Together they form the **identity** forwarded to **`@exellix/ai-tasks`** (`runTask({ jobId, taskId, … })`), graph/node **`eventEmitter`** payloads, structured **`runLog`**, and Activix **`runContext`** / record metadata.

```typescript
import { createExellixGraphRuntime } from '@exellix/graph-engine';

const runtime = createExellixGraphRuntime({
  graphLoader,
  engineFactory,
  tasksClient,
  eventEmitter,           // optional graph/node lifecycle events
  playgroundReporter,     // optional
  runxCreateOptions,      // optional — lazy runx + funcx when graph needs conditional modelConfig / conditions
  // …stepRetryPolicy, runLogMode, concurrency, runTaskDiagnostics, etc.
});

interface GraphExecutionRequest {
  model: GraphModelObject;       // static graph definition
  runtime: GraphRuntimeObject;   // dynamic run state
}

// 2x2 object split:
// Graph model:       GraphModelObject
// Graph runtime:     GraphRuntimeObject
// Task-node model:   TaskNode
// Task-node runtime: TaskNodeRuntimeObject at runtime.nodes[nodeId]

// Task-node runtime: TaskNodeRuntimeObject at runtime.nodes[nodeId] — extensible bag;
// modelConfig / aliasConfig on runtime.nodes were removed in 7.7.

interface GraphRuntimeObject {
  jobId: string;                 // required: host correlation id
  job: any;                      // host envelope: agentId, input, jobType, …
  input?: Record<string, any>;   // active execution input
  jobMemory?: any;
  taskMemory?: any;
  executionMemory?: any;
  variables?: Record<string, any>;
  /** Per-node runtime bag (must not carry modelConfig or aliasConfig since 7.7). */
  nodes?: Record<string, Record<string, unknown>>;
  mode?: 'forward' | 'backward' | 'hybrid';
  goalNodeId?: string;           // required when mode === 'backward'
  debugMode?: boolean;           // include per-node trace on result.debug
  failFast?: boolean;            // default: false
  // …stepRetryPolicy, runLogMode, runtimeObjects, runTaskDiagnostics, etc.
}
```

#### Return shape

```typescript
interface ExecuteGraphResult {
  jobId: string;
  taskId: string;
  graphId: string;
  status: 'completed' | 'failed';
  finalOutput?: unknown;
  finalizerNodeId?: string;
  finalizerType?: string;
  outputsByNodeId: Record<string, unknown>;
  stepsResponses: Record<string, unknown>[];
  engineSnapshot: unknown;
  errors?: Array<{ nodeId?: string; error: unknown }>;
  execution?: unknown;          // includes _trace.nodes
  runLog?: RunLogEntry[];
  runLogTruncated?: boolean;
  runLogOmittedCount?: number;
  logxerCorrelationId?: string;
  debug?: { nodes: NodeTraceEntry[] }; // populated only when debugMode: true
  planAudit?: {
    planHash: string;
    source: string;
  };
  trace?: unknown;
}
```

Host HTTP handlers or workers should accept the same **`GraphExecutionRequest`** shape in the request body so payloads stay aligned with `runtime.executeGraph`.

#### Memory-only playground reporter

Use `createPlaygroundReporter()` when you want rich per-run debugging without filesystem output. The reporter is in-memory only: it does not accept an output directory, does not expose `writeReport`, and never writes request/response payloads to `playground/`, `reports/`, or any other path.

```typescript
import { createExellixGraphRuntime, createPlaygroundReporter } from '@exellix/graph-engine';

const playgroundReporter = createPlaygroundReporter({ runId: 'local-debug-run' });
const runtime = createExellixGraphRuntime({
  graphLoader,
  engineFactory,
  tasksClient,
  playgroundReporter,
});

const plan = compileExellixExecutablePlan(graphModel, graphRuntime);

await runtime.executeGraph({
  plan,
  runtime: graphRuntime,
});

// Inspect full node request/response payloads in memory.
const artifacts = playgroundReporter.getArtifacts();
const snapshot = playgroundReporter.getDebugSnapshot();
const markdown = playgroundReporter.getMarkdown();
```

`getArtifacts()` returns entries such as `01-<nodeId>-request` and `01-<nodeId>-response` with the full payload attached as `payload`. Use `getDebugSnapshot()` when a UI, debugger panel, or test wants steps, artifacts, and rendered markdown as one object.

**Removed in 5.0:** the legacy functional `executeGraph` from `runtime/executeGraph`, the `ExellixGraphClient` class, and their option/result types (`ExecuteGraphOptions`, `ExecuteGraphResponse`, `ExecuteGraphFinalizedResponse`, `ExecuteGraphDebugResponse`, `GraphExecutionResult`). Migrate to `createExellixGraphRuntime(...).executeGraph(...)` — the runtime now covers the same semantics and exposes the same diagnostics through `debugMode: true`. See [`BREAKING-CHANGES.md`](BREAKING-CHANGES.md).

### Run identity (host `jobId` and engine `taskId`)

- **`jobId` (mandatory):** You must pass **`runtime.jobId: string`** on the `executeGraph` input. It must be non-empty after trim. If it is missing or blank, the call fails with **`ExellixGraphErrorCode.JOB_ID_REQUIRED`**. The runtime sets **`job.id`** and **`job.jobId`** to this value for the duration of the run (so templates, events, and memory see a consistent id even if **`job`** omitted **`id`** on input).

- **`taskId` (mandatory on the wire, generated here):** At the start of each `executeGraph`, the engine allocates **`taskId = randomUUID()`**. The same value is attached to **every** `runTask` request in that run (including synthesize-finalizer paths) as **`taskId`**, and is returned on **`ExecuteGraphResult.taskId`**. This satisfies downstream expectations that each graph execution has a stable **per-run** task identity distinct from the host **`jobId`**.

- **Results:** `ExecuteGraphResult` includes both **`jobId`** and **`taskId`** so callers and logs can correlate host scope vs engine run instance.

- **Activix (`@x12i/activix` 8.4+):** Graph-run and node integrations pass a top-level **`runContext`** on every `startRecord` (`sessionId` = **`jobId`**, plus **`taskId`**, **`graphId`**, and for nodes **`nodeId`** / **`skillKey`**). Activix owns **collection tracking state** (`track` / `off` on legend rows) — graph-engine **always** calls lifecycle APIs; do not branch on persistence in app code. In-process maps correlate start → complete/fail by **`activityId`** (`jobId:graphId:taskId` for graph runs; `graphId:nodeId:taskId` for nodes). Query rows via **`activixClient.getJobActivities`** (see [Activix integration](#activix-integration-graph-run-record)).

- **Helpers:** **`assertHostJobId`** and **`newGraphRunTaskId`** are exported from the package root for hosts/tests that build inputs outside `executeGraph`.

**Standalone node debugging:** The runtime also exposes **`runtime.executeNode(...)`** for single-node test runs. Provide `node`, `job`, optional `graph` / `execution`, and (when continuing an existing run) `graphRunTaskId` from the parent `ExecuteGraphResult.taskId` so `runTask` and Activix stay aligned.

### `runTask` request contract (`@exellix/ai-tasks` 8.x)

Graph-engine builds a canonical `RunTaskRequest` for every outbound task call it owns: MAIN task-node invokes, engine PRE/POST utility invokes, and `synthesize` finalizer invokes. MAIN request assembly lives in [`src/runtime/buildAiTasksRunTaskRequest.ts`](src/runtime/buildAiTasksRunTaskRequest.ts); all paths follow **`RUNTASK_REQUEST.md`** in `@exellix/ai-tasks`. Types align with **`RunTaskRequest`** / **`ExellixGraphRunTaskRequest`** exported from this package.

#### Identity and payload

- **Required correlation:** `agentId`, **`jobTypeId`**, **`taskTypeId`** (in addition to `skillKey`, `input`). Defaults: `jobTypeId` ← `job.jobType` or `job.jobTypeId` or `exellix-graph-job`; `taskTypeId` ← `node.taskConfiguration.taskTypeId` or the node’s **`skillKey`**.
- **Canonical task payload:** **`input`** object only (merged execution slice + **materialized** `node.inputs`). Root-level **`question`**, **`raw`**, **`jobInput`**, duplicate **`inputs`**, and legacy **`executionType`** are **not** sent on the request object.
- **Graph telemetry:** `graphId`, **`nodeId`**, **`coreSkillId`** (node id), `masterSkillId`, `masterSkillActivityId`, `jobId`, `taskId`, optional `identity`.

To build `RunTaskRequest` without fallback defaults, the execution request must provide both sides of the contract: `model.id`, `node.id`, `node.skillKey`, explicit `node.taskConfiguration.taskTypeId` (even when it matches `skillKey`), `node.taskConfiguration.executionStrategies` (use `[]` for plain MAIN), `runtime.jobId`, `runtime.job.agentId`, `runtime.job.jobTypeId` or `runtime.job.jobType`, active input/memory, and any model/LLM/diagnostic options needed by the task.

**Model profiles (7.x+):** Graph and node `modelConfig` carry **profile alias names** only (`cheap`, `balanced`, `deep`, or `profile/choice` keys like `cheap/default`, `cyber/deep_forensics`) — never provider model ids in graph JSON. **Since 7.7**, model selection is **graph-document only**: merge `node.taskConfiguration.modelConfig` (partial override per slot) over `model.modelConfig`, then engine defaults (`cheap` / `balanced` / `cheap`). Graph-engine forwards the three-slot triplet `{ preActionModel, skillModel, postActionModel }` on every outbound `runTask`; **`@exellix/ai-tasks`** resolves aliases via `@x12i/ai-profiles`. **7.7.8+** execute-mode validation accepts partial per-task overrides and `profile/choice` encoding without host-side pre-merge. See [`BREAKING-CHANGES.md`](BREAKING-CHANGES.md) §7.6 / §7.7 / §7.7.8.

Graph-engine still derives correlation fields such as `graphId`, `nodeId`, `coreSkillId`, `masterSkillId`, `taskId`, and `masterSkillActivityId` from those authored values.

#### Mandatory `executionStrategies` (breaking vs pre–v7 authoring)

- Every MAIN (and engine PRE/POST utility) **`runTask`** includes **`executionStrategies`**: an array of **`ExecutionStrategyInvocation`** objects (semantics defined by **`@exellix/ai-tasks`** / `RUNTASK_REQUEST.md`).
- **Plain MAIN** (no wrappers): graph-engine sends **`executionStrategies: []`**.
- Optional task-node authoring: **`taskConfiguration.executionStrategies`** — when present and non-empty, it overrides the default **`[]`** for that node’s MAIN call.
- Optional catalog metadata: **`taskConfiguration.executionStrategyCatalogItems`** is forwarded to `ai-tasks`; planner/optimizer rows use **`runtimeKind: "xynthesis-action"`** in ai-tasks **8.5+** (not funcx). `ai-tasks` still validates the runtime invocation shape and consumes only safe catalog fields such as wrapper default sidekick actions.
- **Removed in 5.0:** `metadata.executionStrategyKey` typing and code branches. Configure planners/optimizers through **`executionStrategies`** per ai-tasks (see upstream docs / Catalox task-strategy catalogs).

#### `xynthesized` and internal `execution.xynthesized`

- **Outbound `runTask.xynthesized`:** `{ job, task }` snapshot from durable graph-engine memory — **`job`** = `execution.xynthesized.job`, **`task`** = `execution.xynthesized.taskByNode[nodeId]` (never another node’s task bucket).
- **After a successful MAIN `runTask`**, graph-engine deep-merges **`response.xynthesizedPatch`** into **`execution.xynthesized`** (`patch.job` → `job`, `patch.task` → `taskByNode[nodeId]`).
- On graph start, **`seedGraphRunExecutionState`** ensures **`execution.xynthesized`** exists with **`job`** and **`taskByNode`** objects.

#### `smartInput`

- Optional task-node field **`smartInput`** (`paths: string[]`, optional **`strict`**) is forwarded on **`RunTaskRequest.smartInput`** when set. Paths are validated against graph-engine allowlists (see catalog planning / `validateAiTasksNodeExtensions`).

#### Optional `taskConfiguration` → `runTask` (strategy / Narrix)

Forwarded from `taskConfiguration`: **`narrixMode`**, **`inputStrategyKey`**, **`narrixInput`**, and **`executionStrategyCatalogItems`**. If both **`taskConfiguration.narrix`** and **`taskConfiguration.narrixInput`** are set, set **`taskConfiguration.narrixMode`** to **`preprocessor`** or **`handler`** (see [format doc](.docs/exellix-graph-engine-format.md#metadataaitasks-runtime-strategy--narrix-wiring)).

#### Input bindings before `runTask`

Values in **`node.inputsConfig`** (or deprecated **`node.inputs`**) shaped as **`{ type: 'executionMemoryPath', path }`** (optional **`optional: true`**) or **`{ $path: '…' }`** are resolved against the live **`execution`** object **before** building **`runTask.input`**, so chained graphs see concrete values (e.g. **`graphOutputs.*`**) instead of binding objects.

#### Graph-run execution seeding

When **`executeGraph`** / **`createExellixGraphRuntime().executeGraph`** starts, graph-engine seeds **`runtime.executionMemory.xynthesized`** and mirrors **`runtime.input`** to **`runtime.executionMemory.input`** (flat fields on the object; no `input.raw` wrapper).

**Where hosts put the record:** [`.docs/graph-execution-record-input.md`](.docs/graph-execution-record-input.md) — peer contract (`runtime.input` only, flat paths, matrix vs BFF).

Model knowledge references are resolved inside graph-engine without changing the ai-tasks wire contract. The merge happens when graph-engine builds the outbound **`RunTaskRequest`**: **`model.jobKnowledge`** is applied to the request copy at **`jobMemory.knowledge`**, and **`node.taskKnowledge`** is applied to that task node's request copy at **`taskMemory.knowledge`**. The shared `runtime.jobMemory` / `runtime.taskMemory` objects are not rewritten just to attach model knowledge.

Scope matters: **`jobKnowledge` is graph-run scoped** and is available to every task request through `jobMemory.knowledge`. **`taskKnowledge` is node/task scoped** and must be declared on the task node; root `model.taskKnowledge` is not part of the graph model contract.

#### PRE `inputSynthesis` (authoring → pipeline)

When **`taskConfiguration.aiTaskProfile.inputSynthesis`** is enabled, graph-engine may synthesize or merge a PRE **`synthesized-context`** step (still **one** outbound MAIN `runTask`). Conflicts with an explicit **`node.executionPipeline`** that already defines PRE synthesis are rejected at runtime / validation with **`INPUT_SYNTHESIS_PIPELINE_CONFLICT`**. Details: [.docs/exellix-graph-engine-format.md](.docs/exellix-graph-engine-format.md).

#### Three layers (do not confuse)

1. **Engine PRE/POST strategy utilities (ai-tasks only):** `taskConfiguration.aiTaskProfile.preStrategyKey` / `postStrategyKey` → extra **`runTask`** calls via **`@exellix/ai-tasks`** before/after MAIN; outputs stored at **`execution.xynthesis.pre`** / **`execution.xynthesis.post`** (historical slot names).
2. **`executionPipeline` inside ai-tasks:** e.g. PRE **`synthesized-context`** + MAIN direct — still **one** outbound MAIN `runTask` handled inside `@exellix/ai-tasks`.
3. **Narrix web scope inside ai-tasks:** when **`taskConfiguration.aiTaskProfile.webScoping.enabled`** is true, graph-engine forwards a `narrix` payload with `enableWebScope` / `webScopeQuestions`. The actual web fetch + skip rules run **inside `@exellix/ai-tasks`** (`@x12i/narrix-web-scoper`); graph-engine has **no local web phase** in 5.x.

`synthesize` finalizers are a fourth outbound task-call shape: the finalizer is still a graph model node, but its terminal utility `runTask` uses the same run identity, diagnostics, `llmCall`, and resolved model config as the rest of the graph run.

## Finalizer nodes & Final Response

Executable graphs must include **exactly one** terminal **finalizer node** (`type: "finalizer"`) with **no outgoing edges**.

Required finalizer reads must resolve from the selected memory lane. A non-optional `executionMemoryPath` read must either be seeded by `runtime.executionMemory` before the run starts or be written by a reachable upstream task node through `executionMapping.path`. A non-optional `outputsMemoryPath` read must either be seeded by `runtime.outputsMemory` or be written by a reachable upstream task node through `outputMapping.path`. If the value may be absent because a branch is conditional, mark that finalizer input, section, or item `optional: true`.

The finalizer is a terminal computation or fan-in barrier. It does not own the returned API response. The returned `ExecuteGraphResult.finalOutput` is always resolved from root `model.response` after node `executionMapping`/`outputMapping` writes and finalizer/barrier execution complete:

```ts
type GraphResponseDefinition = {
  missing?: 'omit' | 'null';
  shape: unknown;
};
```

Supported selectors are `outputsMemoryPath`, `executionMemoryPath`, `executionPath`, `nodeMetadata`, `nodeInputsConfig`, `literal`, and `firstPresent`. Legacy `nodeInputs` is still accepted. Missing selector values are omitted by default; `missing: "null"` returns `null` for missing mapped fields.

Example:

```json
{
  "response": {
    "missing": "omit",
    "shape": {
      "answer": {
        "type": "executionMemoryPath",
        "path": "answers.q1.shortAnswer"
      }
    }
  }
}
```

`metadata.graphExecution.outputMode`, finalizer output, and `metadata.graphResponse.responseMapping` are not final-output selectors. Graphs must declare root `graph.response` at persist time; graph-engine does not hoist or migrate legacy response sidecars at compile.

`coreObjective` is resolved once from graph/run context, not from each node response. Supported `sourcePath` roots are `input.*`, `execution.*`, `variables.*` / `jobVariables.*`, `taskVariables.*`, `jobMemory.*`, `taskMemory.*`, and `job.*`.

The selected canonical business output is exposed as:

- `ExecuteGraphResult.finalOutput` (the only API in 5.x).

### Deterministic finalizers (runtime-owned)

Implemented deterministic finalizer types:

- `aggregate`
- `bundle`
- `select`

#### `aggregate` — `strategy: "object-map"`

Build an object by mapping named inputs (from `executionMemoryPath` or literals) into output keys.

#### `aggregate` — `strategy: "report-schema"` (output object assembled from run memory)

Builds the finalizer output object: **`config.sections`** maps each **output field key** to a memory read, using the same `{ type, path }` idiom as finalizer `inputs` and `response.shape` selectors:

```jsonc
"config": {
  "strategy": "report-schema",
  "sections": {
    "q1": { "type": "executionMemoryPath", "path": "answers.q1", "optional": true },
    "summary": { "type": "outputsMemoryPath", "path": "report.summary" }
  },
  "collectEpistemicTags": true
}
```

- Each section: **`type`** (`executionMemoryPath` | `outputsMemoryPath`), **`path`**, and **`optional`** (when `true`, missing values become `null` instead of throwing).
- **`collectEpistemicTags`**: when `true`, walks all section values and collects unique epistemic strings among `CONFIRMED`, `INFERRED`, `ASSUMED`, `UNKNOWN` into the **`collectedTags`** output field (array).

> **Breaking (model v8):** `sections[].path: string` → `sections[].{ type, path }`; `collect_tags` → `collectEpistemicTags`; output `collected_tags` → `collectedTags`. Section `title` and the literal-merge `meta` block were **removed** — authoring titles and studio hints belong in the studio document, and literal output fields belong in `graph.response.shape`. The validator rejects the old keys with a migration message.

Bundled graphs under [`graphs/`](graphs/) (see [graphs/README.md](graphs/README.md)) use this strategy for multi-section reports, sometimes after **`scoped-answer-assembler`** and **`scoped-answer-writer`** persistence (typical store: **`x-scoped-data`** in deployments that use that collection). The graph file shape is defined in [.docs/exellix-graph-engine-format.md](.docs/exellix-graph-engine-format.md).

#### `aggregate` — `strategy: "question-driven"` (generic Q→A formatting)

For “question-driven” graphs, `question-driven` formats the final output by pairing:

- **question text** from the referenced node definition (default path: `inputs.question`)
- **answer** from `executionMemory` at the referenced node’s `outputMapping.path` (or an explicit `answerPath`)

Example:

```json
{
  "id": "finalize",
  "type": "finalizer",
  "finalizerType": "aggregate",
  "inputs": {},
  "config": {
    "strategy": "question-driven",
    "contractVersion": "1",
    "items": {
      "exploitability": { "nodeId": "q2-exploitability" },
      "exposure": { "nodeId": "q5-exposure" },
      "posture": { "nodeId": "q6-posture" }
    }
  }
}
```

Output shape (per item key):

```json
{
  "exploitability": { "question": "...", "answer": { /* whatever the node mapped */ } }
}
```

## Activix integration (graph run record)

Graph-engine depends on **`@x12i/activix`** (see **`package.json`**, currently **8.4.x**). Full wiring guide: [`.docs/activix-records.md`](.docs/activix-records.md). Activix README (collection tracking, `getJobActivities`, phased `outer` / `inner` I/O): `node_modules/@x12i/activix/README.md`.

### Wiring (recommended)

```typescript
import {
  createActivixFromEnv,
  createActivixExellixIntegration,
  createExellixGraphRuntime,
  resolveActivixExellixCollectionNamesFromEnv,
} from '@exellix/graph-engine';
import { buildExellixGraphRuntimeObjects } from '@exellix/graph-engine/testkit';

const ax = createActivixFromEnv({ strict: true });
await ax.init();

const { graphRuns, nodeActivity } = resolveActivixExellixCollectionNamesFromEnv();
const { eventEmitter, activixClient } = createActivixExellixIntegration(ax, {
  activixGraphRun: { collection: graphRuns },
  activixNodeActivity: {
    collection: nodeActivity,
    includeInputSnapshot: true,
    extractNarrixOutcome: true,
  },
});

const runtime = createExellixGraphRuntime({
  graphLoader,
  engineFactory,
  tasksClient,
  eventEmitter,
});

const result = await runtime.executeGraph({
  model: graph,
  runtime: {
    jobId: 'job-123',
    job: { agentId: 'agent-1', jobType: 'analysis' },
    input: entryInput,
    runtimeObjects: buildExellixGraphRuntimeObjects({ graphActivixClient: activixClient }),
  },
});

// Playground / debug UI — no direct Mongo:
const activities = await activixClient.getJobActivities({
  jobId: result.jobId,
  graphId: result.graphId,
  limit: 500,
});
```

**Exports:** `createActivixFromEnv`, `resolveActivixExellixCollectionNamesFromEnv`, `createActivixExellixIntegration`, `createActivixGraphRunIntegration`, `createActivixNodeActivityIntegration`, `createActivixQueryableClient`, `ensureActivixInitialized`, `ensureActivixClientOnRuntimeObjects`, `buildGraphRunActivixRunContext`, `buildNodeActivixRunContext`.

### Collection tracking state (Activix 8.4+)

Legend rows in **`activix-collections`** may set **`state: 'track' | 'off'`**.

| Value | Meaning |
|-------|---------|
| **`track`** (default) | Mongo persistence for that collection |
| **`off`** | Same APIs; Activix skips Mongo (ephemeral in-process rows only) |

**Always** call `startRecord` / `completeRecord` / `failRecord` from integrations — never skip Activix in graph-engine when `off`. Toggle at runtime: `ax.setCollectionTrackingState(collectionName, 'off' | 'track')`. After external DB edits: `ax.refreshCollectionTrackingStates()`.

`createActivixFromEnv()` registers both exellix streams with **`runContext`** indexes and **`collectionRegistry.owner`** = `@exellix/graph-engine`. Collection names default to **`exellix-graph-runs`** and **`exellix-node-activity`** (override with **`ACTIVIX_GRAPH_RUNS_COLLECTION`** / **`ACTIVIX_NODE_ACTIVITY_COLLECTION`**).

### Record shape (graph run)

When using `createActivixGraphRunIntegration()` / `createActivixExellixIntegration()`:

- **Top-level `runContext`** on `startRecord` (not only nested inside custom fields).
- **Canonical response** at `outer.output.response` — only the graph’s **`finalOutput`**.
- **Diagnostics** under `outer.output.data` (nodes, execution, jobMemory, errors, …).
- **Top-level `metadata`** on the row for orchestrator observability (e.g. `graphNodeCount`, `inputVariableKeys`); **`outer.metadata`** carries **`kind`** / **`type`** (`exellix-graph-run`, `graph-run`).

### Graph start — bounded input summaries (Activix)

On `graph:start`, the graph-run integration **does not** persist raw `data.input` / `data.request` (no shallow copy of `variables`, full `GraphExecutionRequest`, or opaque host objects). Activix receives **JSON-serializable summaries** under `outer.input`:

- `inputSummary` — bounded shape for the event’s `input` (e.g. `variables` / memory handles as key counts, shallow primitive previews, nested samples).
- `requestSummary` — same for the merged `request` object emitted with the start event.

Correlation fields (`jobId`, `graphId`, `agentId`, `jobType`, `inputVariableKeys`, `runContext`, etc.) are unchanged.

**Recommended for `job` / `GraphExecutionRequest` / event `data` (Activix-safe):** JSON-like primitives, plain objects/arrays, string ids, `agentId`, `jobType`, and small metadata you are willing to see reflected in summaries.

**Not suitable to rely on for Activix persistence at graph start:** functions, class instances, streams, Buffers, live clients (HTTP/DB), circular graphs, or very large nested payloads — they are summarized or typed (e.g. `shape: "function"`, `shape: "instance"`) and never passed through to `startRecord` as raw references. For large memory, use `includeMemorySnapshots: true` only when you explicitly accept storing `outer.memory.start` / `end`; that remains opt-in and separate from `outer.input`.

## Logxer integration (runtime logs)

Graph-engine uses **`@x12i/logxer`** (^5.1.0) for structured logs and in-process **`getJobLogs`** (playground Logger, diagnostics).

- **Per-run logger:** Each `executeGraph` creates a run-scoped Logxer (`createGraphEngineLogxer({ logging })`) and binds it for the duration of the run.
- **Stack pass-through:** Pass **`logging?: StackLoggingOptions`** on `createExellixGraphRuntime` and/or per `executeGraph` to control downstream packages (`GRAPH_ENGINE`, `AI_TASKS`, …). The run is wrapped in **`runWithAiTasksStackLogging`** from `@exellix/ai-tasks`.
- **Service filter (Logger UI):** Top-level **`runtimeIdentity.service`** identifies the component that emitted the line. Graph-engine native events use **`graph-engine`**. Proxied downstream records keep the **origin** service (e.g. `ai-skills`, `@exellix/ai-tasks`) with wrapper attribution in **`proxyRuntimeIdentity`** — do not rely on **`data.runtimeIdentity.service`** for filtering.
- **Env:** `GRAPH_ENGINE_LOGS_LEVEL` (canonical); bulk stack levels via **`LOGXER_PACKAGE_LEVELS`** (see `@x12i/logxer` docs).

**Node-only Logxer exports:** import from **`@exellix/graph-engine/runtime/logxer`** (not the root barrel — avoids pulling Logxer into browser bundles):

`createGraphEngineLogxer`, `getGraphEngineLogxer`, `createGraphEngineLogxerClient`, `ensureGraphEngineLogxerOnRuntimeObjects`, `runGraphWithLogContext`, `GRAPH_ENGINE_RUNTIME_SERVICE`, `normalizeGraphEngineLogMeta`.

**Browser-safe subpaths:** `@exellix/graph-engine/inspection`, `@exellix/graph-engine/authoring` (authoring gates such as `isAuthoringGraphDocument` — no Logxer).

Wire query surface on **`runtime.runtimeObjects.logxerClient`** (same pattern as Activix — see `buildExellixGraphRuntimeObjects` in testkit).

### `runtime.executeNode(input)`

Execute a single node — useful for tests and for stepping through a node in the context of an existing graph run. Pass `node`, `job`, optional `graph` / `execution`, and (when continuing a parent run) `graphRunTaskId` from the parent `ExecuteGraphResult.taskId` so `runTask` correlation, Activix records, and `runLog` lines stay aligned.

### `loadGraph(graphId)`

Load a graph through the injected loader and validate against the canonical document schema before returning.

## Graph Entry

Graphs declare execution contract under `metadata.graphEntry`. **Data source selection is an operator/work-factory concern** (chosen in jobs-ui at queue time); graphs do not embed input-type or entity-id declarations.

```json
{
  "id": "network-vuln-triage.v1",
  "metadata": {
    "graphEntry": {
      "summary": "Triage one vulnerability record with an optional caller query.",
      "requiredExecutionPaths": ["input.subnetId"]
    }
  },
  "nodes": []
}
```

Use `requiredExecutionPaths` to declare dot-paths the first-wave nodes expect under `runtime.input`. Paths are relative to `GraphRuntimeObject.executionMemory` after `runtime.input` is mirrored (`input.*` root).

## Node Inputs

**Default (graph-engine ≥ 8.x):** Omit **`inputsConfig`** on task nodes when the skill reads flat fields from host **`runtime.input`**. Templates such as `{{input.cveId}}` resolve against flat **`RunTaskRequest.input`**.

Optional **`inputsConfig`** aliases values from `executionMemory` into named skill input keys (e.g. upstream answers). Legacy opt-in: sole **`record → input`** nests the host record under **`runTask.input.record`** — use only when the skill catalog documents **`input.record.*`** templates.

```json
{
  "id": "q1-reachability",
  "skillKey": "professional-answer",
  "taskVariable": {
    "question": "Is this asset reachable from outside the perimeter?"
  }
}
```

**Execution-memory bindings (chaining):** Values in **`node.inputsConfig`** (or deprecated **`node.inputs`**) shaped as **`{ type: 'executionMemoryPath', path }`** (optional **`optional: true`**) or **`{ $path: '…' }`** are resolved against the live **`execution`** object **before** building **`runTask.input`**, so chained graphs see concrete values (e.g. **`graphOutputs.*`**) instead of binding objects.

**Optional `smartInput`:** Top-level on the task node (sibling to **`inputsConfig`**): **`{ "paths": ["input", "graphOutputs.prev"], "strict": true }`** — forwarded on **`RunTaskRequest.smartInput`**; paths must satisfy graph-engine allowlists.

**Rules:**

- `node.taskVariable.question` — the question text sent to the skill template. Always set it under **`taskVariable`** (not `inputsConfig`).
- Per-node dynamic values from execution memory belong in **`inputsConfig`** when aliasing is required; omit **`inputsConfig`** for flat host input passthrough.
- Path references in `executionMapping` use execution-memory paths written by upstream nodes.
- Do **not** use deprecated sole **`inputsConfig.record → input`** when **`webQueryTemplates`** use flat **`{{input.<field>}}`** tokens.

**What does NOT belong in `inputsConfig`:**

- `skillKey` — top-level node field.
- `taskConfiguration.narrix` — NARRIX engine config (`datasetId`, `questionId`, `layer`, etc.). These are routing/filter fields consumed by the NARRIX pre-processor, not by the template.
- `taskConfiguration.aiTaskProfile.webScoping` — opt-in web context. Graph-engine forwards web intent on the outbound `narrix` payload; actual web fetch and skip rules run inside `@exellix/ai-tasks`. See [.docs/exellix-graph-engine-format.md — Web scoping behavior](.docs/exellix-graph-engine-format.md#web-scoping-behavior).
- Template variables — use `node.variables` + paths `taskVariables.*`, or `model.variables` + `jobVariables.*` (see [Variables (two buckets)](#variables-two-buckets--job--task)). Use `node.taskVariable` for prompts, not `inputs`.

> `taskConfiguration.narrix.questionId` (e.g. `"q1"`, `"q6"`) is a NARRIX internal routing key that tells the pre-processor which framework question slot this node fills. It has nothing to do with `inputs.question` (the human-readable question text). Do not confuse them.

**First-class: question and questionId.** For NARRIX nodes you can set `inputs.question`, or `taskConfiguration.narrix.questionId`, or both. Once NARRIX provides a question ↔ questionId resolver, the missing one can be filled automatically. See [.docs/question-questionId-first-class-experience.md](.docs/question-questionId-first-class-experience.md).

## Skill Key Resolution

Canonical graph models must specify `node.skillKey` for remote task nodes. Legacy aliases `node.data.skillKey` and `node.metadata.skillKey` are rejected by canonical validation. Optional fallback to `tasks/${node.id}` exists only when `allowFallbackToNodeId: true` is explicitly enabled (default: **disabled**).

If `skillKey` is missing, `exellix-graph-engine` throws `NODE_SKILLKEY_MISSING` error **before** execution begins.

### Configuration

```typescript
skillKeyResolution: {
  allowFallbackToNodeId?: boolean; // default: false (strict mode)
  fallbackPrefix?: string;         // default: "tasks/"
  aliases?: Record<string, string>; // optional explicit remaps
}
```

## Variables (two buckets — job + task)

Template variables are **not** merged into one bag. Graph-engine mirrors two scopes on `executionMemory`:

| Scope | Runtime inputs | Execution mirror | Memory paths |
|-------|----------------|------------------|--------------|
| **Job / graph** (whole run) | `model.variables`, `runtime.variables`, `runtime.jobVariables`, legacy `job.jobVariables` | `execution.jobVariables` | `jobVariables.*` (legacy alias `variables.*`) |
| **Task / node** (current node) | `node.variables`, `runtime.taskVariables` | `execution.taskVariables` | `taskVariables.*` |

Outbound `runTask` sends **`variables`** (ai-tasks field name) = job bucket only; node scope stays on **`executionMemory.taskVariables`**. Requires **graph-engine ≥ 5.13** and **@exellix/ai-tasks ≥ 7.6.2** (passthrough, no flattening).

> **Important**: graph-engine does not define skill templates; it forwards buckets as-is. See [`formats-documentations/graph-runtime-object-format.md`](formats-documentations/graph-runtime-object-format.md).

### Upstream template rendering (`ai-gateway` / `athenix-parser`)

The stack that eventually renders skill prompts pulls in **`@athenices/ai-gateway`** (transitively via `@exellix/ai-tasks` / `@exellix/ai-skills`), which depends on **`@athenices/athenix-parser` v4+**. That parser implements the **v4 template protocol**: required `{{path}}` values throw **`TemplateResolutionError`** when missing; optional tokens use **`{{path |}}`** or **`{{path | fallback text}}`**; **`subPathSearch`** is opt-in for alternate root lookup. See the parser’s README under `node_modules/@athenices/ai-gateway/node_modules/@athenices/athenix-parser/` when debugging template failures.

**Web research in prompts:** when this repo runs **local** question-driven web scope in `executeNode`, it still injects a flat **`webContextMarkdown`** string into `jobContext` so templates can use `{{#if webContextMarkdown}}` with a **bounded** markdown block. That remains the default pattern here even though deep `execution.*` paths are now supported upstream if you pass full context and configure the gateway/parser accordingly.

Web scoping now runs inside `@exellix/ai-tasks`, not as a local graph-engine web-scoper phase. Author web intent under **`taskConfiguration.aiTaskProfile.webScoping`**; downstream ai-tasks controls source snippets, markdown, skip rules, and any enrichment from `execution.input`.

## Memory Wiring

Memory is passed through consistently:

- `jobMemory = runtime.jobMemory`; model knowledge is added only to the outbound `RunTaskRequest.jobMemory.knowledge` copy.
- `taskMemory = runtime.taskMemory`; node task knowledge is added only to that node's outbound `RunTaskRequest.taskMemory.knowledge` copy.

Optional per-node memory overrides:
- `node.memory?.taskMemory`
- `node.memory?.jobMemory` (rare, but allowed)
- `node.jobContextMapping` (see below)

## Input Grouping (`inputs`)

Nodes can group their dynamic inputs under an `inputs` object for better structure and to avoid conflicts with system fields.

```json
{
  "id": "node-1",
  "type": "task",
  "skillKey": "my-skill",
  "inputs": {
    "question": "What is the capital of France?",
    "detailed": true
  }
}
```

## Job Context Mapping (`jobContextMapping`)

`jobContextMapping` allows a node to pull specific data from `jobMemory` into its local execution context (`jobContext`).

```json
"jobContextMapping": {
  "map": {
    "customer": "jobMemory.profiles.current",
    "history": "jobMemory.activityLog"
  }
}
```

The runtime will resolve these paths from `jobMemory` and provide a `jobContext` object to the underlying task.

Path expressions in `map` support:
- **Dot paths**: `jobMemory.profiles.current`, `executionMemory.graphOutputs.vulnInstances.records`
- **Array wildcard** `[*]`: e.g. `executionMemory.graphOutputs.vulnInstances.records[*]` — selects all array elements
- **Object values** `{*}`: e.g. `recordsById{*}` — selects all values of an object (deterministic key order)

Sources can be `jobMemory.*` or `executionMemory.*` (current execution state from previous nodes).

## Execution Object & Trace

### Execution Object

The execution object is a shared state container that persists across all nodes in a graph execution. It stores:
- **Output mappings**: Data written by nodes via `outputMapping`
- **`xynthesized` memory**: **`execution.xynthesized.job`** (run-wide) and **`execution.xynthesized.taskByNode[nodeId]`** (per node). Outbound **`runTask.xynthesized`** exposes the current node’s slice; **`xynthesizedPatch`** on responses is merged after successful MAIN calls.
- **Execution trace**: Per-node execution metadata (see below)
- **Custom state**: Any application-specific data

The execution object is passed to each node as `executionMemory` in the task request and updated through `outputMapping` configurations. Edge predicates may also read **`input.*`** and **`xynthesized.*`** roots via the same evaluation context (see [`src/runtime/predicates.ts`](src/runtime/predicates.ts)).

### Execution Trace (FR-6)

Each graph run records a **structured trace** per node in the execution object:

- **Location**: `execution._trace.nodes[nodeId]`
- **Shape**: `{ startedAt, endedAt, skillKey, ok, durationMs, activityId?, summary?, error? }`

**Trace fields:**
- `startedAt`: Unix timestamp (ms) when node execution started
- `endedAt`: Unix timestamp (ms) when node execution ended
- `skillKey`: The resolved skill key for this node
- `ok`: `true` if successful, `false` if failed
- `durationMs`: Execution duration in milliseconds
- `activityId`: Optional activity/task identifier from ai-tasks
- `summary`: Optional metadata from task response (on success)
- `error`: Error details with `message`, optional `code`, and optional `stack` (on failure)

Trace is written by the runtime for every node (both `executeGraph` and `createExellixGraphRuntime` flows), regardless of success or failure.

### Execution Object in Events (FR-3)

All node execution events include the execution object merged into `jobMemory`:

- **Node Start Event**: `input.jobMemory.execution` contains the current execution state
- **Node Complete Event**: `memoryAfter.jobMemory.execution` contains the updated execution state (after outputMapping)
- **Node Fail Event**: `memoryAfter.jobMemory.execution` contains the execution state at the time of failure

This ensures consistent tracking and allows activity tracking systems to access execution data for monitoring and debugging.

### ai-tasks metadata for trace

For rich structured trace, local tasks (ai-tasks) should return metadata in the response so the graph can store it in `execution._trace.nodes[nodeId].summary` and use it for `durationMs` / `activityId`:

- **`response.parsed.meta.durationMs`** — task duration in milliseconds (otherwise the runtime uses `endedAt - startedAt`)
- **`response.parsed.meta.localTaskId`** or **`response.parsed.meta.activityId`** — optional activity/handler identifier

MAIN **`runTask`** traces may also include bounded summaries of **`smartInput.paths`** and keys touched by **`xynthesizedPatch`** (not full synthesized payloads).

Any other fields in `response.parsed.meta` are stored in `summary`.

## Contextual Knowledge Scope (`scope`)

Nodes can define complex filtering rules to scope lists from memory into the node's context. This is useful for building localized knowledge for a task (e.g., "all assets in the same zone as the target").

```json
{
  "scope": {
    "contextualKnowledge": [
      {
        "list": "zonesEnriched",
        "select": ["zone", "semantics.trust_level"],
        "filter": {
          "where": [
            { "path": "virtual_router", "eq": "{{asset.virtual_router}}" },
            { "path": "containsTargetIp", "eq": true }
          ],
          "limit": 10
        }
      }
    ]
  }
}
```

**Features:**
- **Dynamic Filtering**: Use placeholders like `{{asset.zone}}` to reference variables or memory.
- **Operators**: `eq`, `eqAny` only (precompute domain-specific fields in the host when needed).
- **Selection**: Pick specific fields using `select` to minimize context size.
- **Integration**: The result is merged into `jobContext` alongside `jobContextMapping`.

## Backward Planning

When `mode === "backward"`:

- `goalNodeId` is **required**
- `goalNodeId` must exist in the graph

If missing/invalid, `exellix-graph-engine` throws `BACKWARD_GOAL_REQUIRED` error **before** execution begins.

## Error Handling

All errors are structured **`ExellixGraphError`** instances with:

- `code`: **`ExellixGraphErrorCode`** (e.g., `NODE_SKILLKEY_MISSING`, `TASK_NOT_FOUND`)
- `message`: Human-readable message
- `context`: Additional context (jobId, graphId, nodeId, etc.)

### Task Not Found

When `ai-tasks.runTask()` returns "not found":

- Node is marked as `failed` with `reason: "TASK_NOT_FOUND"`
- Includes `skillKey` and ai-tasks diagnostics payload **as-is**
- Commits failure to graphenix (so history contains it)
- Then:
  - If `failFast=true` → abort graph
  - Else continue if graph allows it

## Understanding Task Content

To avoid confusion across the stack:

- **Node `skillKey`** identifies the **task** (ai-tasks layer)
- The task will reference or embed a **skill** (ai-skills layer)
- If content is missing, it's either:
  - Missing `node.skillKey` → `exellix-graph-engine` error (`NODE_SKILLKEY_MISSING`)
  - Task missing → `ai-tasks` not found (`TASK_NOT_FOUND`)
  - Skill missing inside task → `ai-tasks`/`ai-skills` error

`exellix-graph-engine` surfaces these errors; it doesn't decide what tasks/skills exist.

## Types

```typescript
import type {
  Graph,
  GraphModelObject,
  GraphAiModelConfig,
  GraphModelAliasConfig,
  GraphNode,
  TaskNode,
  TaskNodeRuntimeObject,
  TaskNodeTaskConfiguration,
  Job,
  ExecuteGraphInput,
  GraphExecutionRequest,
  GraphRuntimeObject,
  ExecuteGraphResult,
  ExellixGraphRunTaskRequest,
  ExellixGraphRunTaskResponse,
  BuildAiTasksRunTaskRequestArgs,
  ExecutionStepOption,
  SmartInputConfig,
  ExecutionStrategyInvocation,
  XynthesizedMemory,
} from '@exellix/graph-engine';
```

**`RunTask` wire:** `ExellixGraphRunTaskRequest` / `ExellixGraphRunTaskResponse` are aliases of `@exellix/ai-tasks` **`RunTaskRequest`** / **`RunTaskResponse`**. **`SmartInputConfig`**, **`ExecutionStrategyInvocation`**, **`XynthesizedMemory`**, and related strategy / xynthesized types are **re-exported from `@exellix/ai-tasks`** via [`src/types/aiTasksDerivedTypes.ts`](src/types/aiTasksDerivedTypes.ts) so you can import stable names from **`@exellix/graph-engine`** without duplicating `NonNullable<RunTaskRequest[…]>` aliases.

## Graph run persistency contract

Graph authoring documents declare writeback intent under `graph.response.persistency`. Hosts (`@exellix/jobs`, `@exellix/jobs-api`) read that contract **before** dispatch to resolve Memorix persistency targets and to validate assembled runtime input.

```typescript
import {
  resolveGraphRunContract,
  parseGraphRunPersistency,
  mergeGraphRunPersistency,
  type GraphRunPersistency,
} from '@exellix/graph-engine';

const contract = resolveGraphRunContract(graphId, authoringDocument);
// contract.persistency?.targetEntityName — concrete entity or 'same-as-input'
// contract.persistency?.recordMode — 'always-new' | 'upsert-by-source'
// contract.persistency?.contentType — e.g. 'inferences'
```

Exported helpers: `resolveGraphRunContract`, `loadGraphRunContracts`, `parseGraphRunPersistency`, `mergeGraphRunPersistency`, `applyGraphRunContractOverride`, `readGraphDocumentVersion`. Tests: `tests/graph-run-contract.test.ts`.

## Error Codes

Structured errors are **`ExellixGraphError`** with **`ExellixGraphErrorCode`**:

```typescript
import { ExellixGraphErrorCode } from '@exellix/graph-engine';

// Examples: JOB_ID_REQUIRED, NODE_SKILLKEY_MISSING, BACKWARD_GOAL_REQUIRED,
// GRAPH_LOAD_FAILED, GRAPH_EXECUTION_FAILED, NODE_EXECUTION_FAILED,
// TASK_NOT_FOUND, INVALID_GRAPH, INVALID_NODE, NON_CANONICAL_GRAPH_DOCUMENT,
// NON_CANONICAL_TASK_NODE
```

Validation issues for **`smartInput`** / **`inputSynthesis`** during catalog planning use string codes such as **`SMART_INPUT_PATHS_INVALID`**, **`INPUT_SYNTHESIS_PIPELINE_CONFLICT`**, **`INPUT_SYNTHESIS_DESTINATION_INVALID`** (see [`src/inspection/validateAiTasksNodeExtensions.ts`](src/inspection/validateAiTasksNodeExtensions.ts)).

## Repository Structure

```
src/
  index.ts
  runtime/ExellixGraphRuntime.ts
  runtime/buildAiTasksRunTaskRequest.ts
  runtime/aiTasksStrategyPhases.ts
  runtime/graphRunExecutionSeed.ts
  runtime/resolveExecutionPipelineForTaskNode.ts
  runtime/localSkills/
  runtime/variables.ts
  runtime/memory.ts
  runtime/events.ts
  loaders/FileGraphLoader.ts
  types/refs.ts
  types/options.ts
  types/aiTasksDerivedTypes.ts
  types/results.ts
  errors/ExellixGraphError.ts
  errors/exellixGraphErrorCodes.ts
```

## Testing

- **`npm test`** — Generic SDK checks only: deterministic finalizers, graph inspection, contract inspection (`src/tests/run-tests.ts`). Does not run bundled graph JSON from `graphs/`.
- **`npm run test:graphs`** — Question-breakdown sample graph (real `ai-tasks`).
- **`npm run test:subnet`** — `narrix-subnet-egress-triage.v1` with sample data from `graphs/tests/examples/`.
- **Other graph runners** (`dod`, `run:vuln-group:trace`, `test:web-scope-e2e`, …) live under **`graphs/tests/`**; see [graphs/tests/README.md](graphs/tests/README.md).

Fixtures and DOD outputs for those graphs are under **`graphs/tests/`** (`examples/`, `realdata/`, `outputs/`). The top-level **`tests/`** folder only documents the move (see [tests/README.md](tests/README.md)).

## Example graphs (`graphs/`)

**Graph JSON shape:** [.docs/exellix-graph-engine-format.md](.docs/exellix-graph-engine-format.md).

Bundled **graph definitions** (JSON DAGs) live in [`graphs/`](graphs/). **[graphs/README.md](graphs/README.md)** is the **bundle catalog** (graph IDs, product-specific notes, execution call grid, appendix for bundled v2 graphs, and **[Platform contract: respected target](graphs/README.md#platform-contract-respected-target-for-graphs-and-integrations)** for integration baseline and feature requests).

To execute a graph, supply a `graphLoader` that resolves `graphId` to JSON, build a `job` with the input shape your graph expects (often `execution.input` with `raw` and optional `metadata`), and call `executeGraph`. Further integration notes may live under [.docs/](.docs/).

**`taskConfiguration.aiTasksOutputValidation`** on task nodes is forwarded as **`outputValidation`** on the `runTask` request (**`@exellix/ai-tasks` 8.x**). Root **`outputConstraints`** is not part of the closed schema.

## Runtime observability (`runtimeObjects`)

Hosts and playgrounds compose a single **`runtimeObjects`** tree instead of querying Mongo or Logxer stores directly:

| Client | Source | Query API |
|--------|--------|-----------|
| **`activixClient`** | Your `Activix` instance via `createActivixQueryableClient` / `createActivixExellixIntegration` | `getJobActivities({ jobId, graphId?, nodeId?, limit? })` |
| **`logxerClient`** | Per-run graph-engine Logxer via `ensureGraphEngineLogxerOnRuntimeObjects` | `getJobLogs({ jobId, graphId?, nodeId?, level?, … })` |
| **`packagesRuntimeObjects`** | Optional subtree from `@exellix/ai-tasks` | Package-owned clients when exported |

Use **`buildExellixGraphRuntimeObjects({ graphActivixClient, graphLogxerClient, aiTasksRuntimeObjects })`** (testkit) and pass the result on **`runtime.runtimeObjects`**. **`setRuntimeObjectsLastJobId`** is applied automatically during `executeGraph`.

## Integration with Other Packages

See `.reports/` directory for request documents:

- `graphenix-request-node-contract.md` - Graph node contract
- `ai-tasks-request-not-found-contract.md` - Task not found handling
- `ai-tasks-request-variable-channels.md` - Variable flow documentation
- `exellix-helpers-requests.md` - Helper utilities

## License

ISC

---
_Source: https://npm.io/package/@exellix/graph-engine · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
