npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@exellix/graph-engine

v7.0.2

Published

Graph executor SDK

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({ model, runtime }) invocation, it runs exactly one graph run (validate model → plan → 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 the static model: GraphModelObject, 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 graph model: The caller supplies model: GraphModelObject directly in the execution request. The effective correlation graphId on the result and in telemetry is model.id.
  2. plan = graphenix.plan(...)
  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).

Specification & documentation

| Topic | Document | |--------|----------| | Object contracts (model × runtime + per-call resolution view) | Index: formats-documentations/README.mdgraph-model, graph-runtime, task-node-model, task-node-runtime, ExellixRuntimeObject | | Ecosystem acceptance (ai-tasks, studio, matrix parity) | .docs/ecosystem-acceptance-criteria.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 narrixrunTask), graph JSON vs outbound runTask) | .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 | | Layer 01 / 08 graph entry & response contracts | .docs/graph-io-visibility.md | | Graph entry dataFilters v1 / public evaluator | .docs/data-filters-evaluation.md | | Task-node conditions + conditional modelConfig.cases (runx) | .docs/task-node-conditions-evaluation.md | | Model profile aliases (7.x: graph = profile names, runtime = concrete models) | BREAKING-CHANGES.md §7.0.0, .docs/ai-tasks-model-profile-aliases-7x.md (ai-tasks), .docs/fr-model-alias-descriptors.md (upstream FRs) | | Platform vs implementation (no domain operators in schema) | .docs/platform-generic-vs-implementation.md | | Bundled graph examples & bundle README | 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 section. Graph-engine integrates with @exellix/ai-tasks at the semver range declared in package.json (currently ^7.6.4); 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 or invoke the Xynthesis SDK directly.

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, aliasConfig, runtime node overrides) belongs under the execution request’s runtime object and is rejected on the model.

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 still comes from executeGraph({ model, runtime }).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

npm install @exellix/graph-engine

Upstream tasks SDK: This package depends on @exellix/ai-tasks ^7.4.0 (see package.json). Graph-engine emits RunTaskRequest shapes that match the v7 closed schema (mandatory executionStrategies, 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 (@exellix/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 for how you should inject runtime.executeGraph, resolve metadata.graphEntry per model, seed runtime.executionMemory, and pass runtime.jobId (see Run identity below). Helpers such as buildMatrixJobForGraphRun align id and job.jobId on the job object so you can call runtime.executeGraph({ model, 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 — copy to .env at the project root for local tests and scripts that load dotenv.
  • Semantics: .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), 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 ≥ 7.6 preflight surface so studios and matrix hosts can validate a node before execution without a second dependency:

| 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. | | analyzeTaskNodeRunTaskRequest | Catalox-backed skill request analysis (templates, gateway packet preview) plus optional config validation. |

Lower-level helpers (validateRunTaskConfig, validateRunTaskInvoke, analyzeRunTaskRequest, analyzeSkillRequest, formatSkillRequestAnalysisMarkdown, Rendrix listTokens / analyzeTemplateResolution, …) are re-exported from the package root when you already have a RunTaskRequest.

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

import { 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)
});

// Host correlation id (required). Engine sets `job.id` / `job.jobId` from it and generates `result.taskId`.
const result = await runtime.executeGraph({
  model: graphModel,
  runtime: {
    jobId: 'job-123',
    job: { agentId: 'agent-1', input: {} },
    input: { question: 'Analyze this record' },
    // 7.x: graph modelConfig values are profile aliases; concrete models bind here (required).
    modelConfig: {
      cases: [{ modelConfig: { xynthesisModel: 'weak', skillModel: 'strong' } }],
    },
    aliasConfig: {
      strong: 'anthropic/claude-sonnet-4',
      weak: 'google/gemini-2.5-flash',
      default: 'google/gemini-2.5-flash',
    },
  },
});

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

Public API

runtime.executeGraph({ model, 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 static model: GraphModelObject and a dynamic runtime: GraphRuntimeObject. 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.

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

const runtime = createExellixGraphRuntime({
  graphLoader,
  engineFactory,
  tasksClient,
  modelConfig,            // optional explicit fallback: { xynthesisModel, skillModel }
  eventEmitter,           // optional graph/node lifecycle events
  playgroundReporter,     // optional
  // …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]

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>;
  /** Run-level model profile override (`cases`); values are alias names, not provider ids. */
  modelConfig?: { cases: Array<{ when?: unknown; modelConfig: { xynthesisModel: string; skillModel: string } }> };
  /** Required (7.x): profile alias → concrete provider model id for this execution. */
  aliasConfig: Record<string, string>;
  nodes?: Record<string, {
    /** Per-node profile override (alias names). */
    modelConfig?: { xynthesisModel: string; skillModel: string };
    /** Per-node alias bindings (overlay `aliasConfig`). */
    aliasConfig?: Record<string, string>;
  }>;
  mode?: 'forward' | 'backward' | 'hybrid';
  goalNodeId?: string;           // required when mode === 'backward'
  debugMode?: boolean;           // include per-node trace on result.debug
  failFast?: boolean;            // default: false
  // …llmCall, stepRetryPolicy, runLogMode, runtimeObjects, runTaskDiagnostics, etc.
}

Return shape

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
  graphAudit?: {
    source: 'model';
    contentSha256: string; // stable JSON + SHA-256 of supplied model (audit / matrix persistence)
  };
}

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.

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

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

await runtime.executeGraph({
  model,
  runtime: {
    jobId: 'job-123',
    job,
    input,
  },
});

// 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.

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: Graph-run integration persists jobId, taskId, and graphId on the record and in runContext (with sessionId still aligned to jobId for Activix v5). The in-memory correlation key for start → complete/fail is jobId:graphId:taskId so concurrent retries of the same host job against the same graph do not collide. Node activity integration keys rows by graphId:nodeId:taskId and includes taskId in runContext and top-level metadata.

  • 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 v7.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; 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: jobTypeIdjob.jobType or job.jobTypeId or exellix-graph-job; taskTypeIdnode.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 (strong, weak, default, …) — never provider model ids in graph JSON. runtime.aliasConfig is required and maps every alias used by the run (including default when fallback applies) to concrete provider models. Selection order: runtime.nodes[nodeId].modelConfignode.taskConfiguration.modelConfigruntime.modelConfigmodel.modelConfig → implicit { default, default }; then strict resolution through runtime.aliasConfig plus runtime.nodes[nodeId].aliasConfig. The resolved { xynthesisModel, skillModel } is forwarded to MAIN, engine PRE/POST utility calls, and synthesize finalizer calls. Snapshot aliasConfig on execution records for reproducibility. See BREAKING-CHANGES.md §7.0.0.

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; ai-tasks still validates the runtime invocation shape and consumes only safe catalog fields such as wrapper default function ids.
  • 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.jobjob, patch.tasktaskByNode[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 taskConfigurationrunTask (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).

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 — 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.

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 (@exellix/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:

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:

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

metadata.graphExecution.outputMode, finalizer output, and legacy metadata.graphResponse.responseMapping are not final-output selectors for new graphs. Migrate legacy response mappings by copying missing and shape to root response and dropping version, target, primaryResponsePaths, debugResponsePaths, notableExecutionPaths, and mappingPreset.

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

aggregatestrategy: "object-map"

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

aggregatestrategy: "report-schema" (multi-section report from execution memory)

Builds a single object whose keys come from config.sections: each section specifies a dot-path into executionMemory (path), optional title, and optional (when true, missing values become null instead of throwing).

  • collect_tags: when true, walks all section values and collects unique epistemic strings among CONFIRMED, INFERRED, ASSUMED, UNKNOWN into collected_tags (array).
  • meta: optional literal key/value pairs merged at the top level of the parsed output.

Bundled graphs under graphs/ (see 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.

aggregatestrategy: "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:

{
  "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):

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

Activix integration (graph run record)

When using createActivixGraphRunIntegration() / createActivixExellixIntegration():

  • Canonical response is stored at outer.output.response and is only the graph’s finalOutput.
  • Detailed execution data (nodes, execution memory, errors, etc.) is stored under outer.output.data.

This keeps the “business output” clean while preserving full diagnostics.

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.

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 Inputs

Graph JSON can declare the semantic type of input it is designed to work with under metadata.graphEntry.inputs. This is part of the graph object itself and is intended for authors, catalogs, dashboards, and future validators; the runtime does not validate or coerce it today.

{
  "id": "network-vuln-triage.v1",
  "metadata": {
    "graphEntry": {
      "summary": "Triage one vulnerability record with an optional caller query.",
      "inputs": [
        { "kind": "record", "path": "input", "required": true },
        { "kind": "query", "path": "input.query", "required": false }
      ],
      "requiredExecutionPaths": ["input.subnetId"]
    }
  },
  "nodes": []
}

Standard kind values are record, query, user-input, content, metadata, and execution. Use multiple entries for combinations, for example a graph that needs both a raw record and a user question. Paths are relative to the merged execution object, so input maps to GraphRuntimeObject.executionMemory.input after runtime.input is mirrored.

Use kind: "execution" when the graph is designed to consume a prior graph execution. The design contract names the source graphId and an optional metadataFilter for fields such as status values:

{
  "kind": "execution",
  "graphId": "network-vuln-group-triage.v1",
  "metadataFilter": { "status": "completed" },
  "path": "input.upstreamExecution",
  "required": true
}

The runtime does not perform that lookup today; host tooling resolves it and passes the selected execution under the declared path.

Node Inputs

All dynamic per-node values — including the question text sent to the template — must be defined under node.inputs. This is the single canonical location.

{
  "id": "q1-reachability",
  "skillKey": "professional-answer",
  "inputs": {
    "question": "Is this asset reachable from outside the perimeter?",
    "record": { "type": "executionMemoryPath", "path": "input" }
  }
}

Execution-memory bindings (chaining): Objects { "type": "executionMemoryPath", "path": "graphOutputs.upstream.payload" } (and { "$path": "executionMemory.…" } / input.… forms) are resolved against the live execution object before runTask. The outbound request carries scalar/object values under input, not the binding shape. Use optional: true on executionMemoryPath bindings if the path may be missing.

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.inputs.question — the question text sent to the skill template as {{question}}. Always set it here.
  • Any other per-node dynamic value (e.g. record, context) also belongs in inputs.
  • Path references in outputMapping use node.inputs.question (e.g. "question": "node.inputs.question").
  • node.inputs is the only place the runtime reads question from. There is no fallback to node.question or node.data.question.

What does NOT belong in inputs:

  • 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.
  • Template variables — use node.variables + paths taskVariables.*, or model.variables + jobVariables.* (see Variables (two buckets)). 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.

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

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.

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.

{
  "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).

"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).

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").

{
  "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.skillKeyexellix-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

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 so you can import stable names from @exellix/graph-engine without duplicating NonNullable<RunTaskRequest[…]> aliases.

Error Codes

Structured errors are ExellixGraphError with ExellixGraphErrorCode:

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).

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:subnetnarrix-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.

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).

Example graphs (graphs/)

Graph JSON shape: .docs/exellix-graph-engine-format.md.

Bundled graph definitions (JSON DAGs) live in graphs/. 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 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/.

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

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