# @exellix/jobs

> Exellix jobs manager — programmatic runtime for the durable job queue (enqueue, claim, complete, fail, runWorker, pollers, Memorix integration). HTTP API is @exellix/jobs-api; dashboard is @exellix/jobs-ui. Persistence via @exellix/jobs-db.

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

## Install

```sh
npm install @exellix/jobs
pnpm add @exellix/jobs
yarn add @exellix/jobs
bun add @exellix/jobs
```

Provides the command `exellix-jobs`.

## 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 | 1.2.0 |
| Published | 2026-07-11 |
| First published | 2026-06-14 |
| Weekly downloads | 0 |
| License | exellix-license |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Node | >=20 |
| Dependencies | 16 |
| Unpacked size | 709.7 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | exellix |
| Keywords | exellix, jobs, queue, worker |

## Links

- npm: https://www.npmjs.com/package/@exellix/jobs
- 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/jobs

## Dependencies (16)

- [mongodb](https://npm.io/package/mongodb.md) ^6.21.0
- [@x12i/logxer](https://npm.io/package/@x12i/logxer.md) ^5.1.0
- [@x12i/activix](https://npm.io/package/@x12i/activix.md) ^9.0.3
- [@exellix/jobs-db](https://npm.io/package/@exellix/jobs-db.md) ^1.1.0
- [@x12i/memorix-mongo](https://npm.io/package/@x12i/memorix-mongo.md) 1.32.0
- [@exellix/error-intel](https://npm.io/package/@exellix/error-intel.md) ^1.0.2
- [@x12i/memorix-hippox](https://npm.io/package/@x12i/memorix-hippox.md) 1.33.0
- [@x12i/memorix-writer](https://npm.io/package/@x12i/memorix-writer.md) 1.33.0
- [@exellix/graph-engine](https://npm.io/package/@exellix/graph-engine.md) ^9.6.0
- [@x12i/memorix-pipeline](https://npm.io/package/@x12i/memorix-pipeline.md) 1.33.0
- [@exellix/catalox-graphs](https://npm.io/package/@exellix/catalox-graphs.md) ^1.5.0
- [@exellix/job-dispatcher](https://npm.io/package/@exellix/job-dispatcher.md) ^1.0.11
- [@x12i/countex-pipelines](https://npm.io/package/@x12i/countex-pipelines.md) ^1.0.1
- [@x12i/memorix-assembler](https://npm.io/package/@x12i/memorix-assembler.md) 1.32.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

## Alternatives

- [cron](https://npm.io/package/cron.md) — 4.9M weekly downloads
- [@vercel/queue](https://npm.io/package/@vercel/queue.md) — 731.6K weekly downloads
- [create-sonicjs](https://npm.io/package/create-sonicjs.md) — 1.6K weekly downloads
- [@exellix/jobs-api](https://npm.io/package/@exellix/jobs-api.md) — 941 weekly downloads
- [@forwardimpact/libskill](https://npm.io/package/@forwardimpact/libskill.md) — 575 weekly downloads

## Recent versions

- 1.2.0 (latest) — 2026-07-11
- 1.1.0 — 2026-07-11
- 1.0.24 — 2026-07-10
- 1.0.23 — 2026-07-07
- 1.0.22 — 2026-07-07
- 1.0.21 — 2026-07-07
- 1.0.20 — 2026-07-07
- 1.0.19 — 2026-07-07
- 1.0.18 — 2026-07-05
- 1.0.17 — 2026-07-05
- 1.0.16 — 2026-07-05
- 1.0.15 — 2026-07-01
- 1.0.11 — 2026-06-30
- 1.0.10 — 2026-06-26
- 1.0.9 — 2026-06-26
- … 9 more at https://npm.io/package/@exellix/jobs/versions

## README

# @exellix/jobs

**Jobs manager** — the programmatic runtime for Exellix’s durable job queue: `enqueue` / `claim` / `complete` / `fail`, plus `runWorker`, source pollers, dependency wiring, retry-with-backoff, and lease recovery. Import this package from workers, scripts, and other services.

| Package | Role |
|---------|------|
| **`@exellix/jobs`** (this) | Jobs **manager** — queue logic, worker loop, CLI (`exellix-jobs`) |
| [`@exellix/jobs-api`](../jobs-api/README.md) | Jobs **HTTP API** — Fastify REST + programmatic queries |
| [`@exellix/jobs-ui`](../jobs-ui/README.md) | Jobs **dashboard** — React operator SPA |
| [`@exellix/jobs-db`](../jobs-db/README.md) | Jobs **data tier** — Mongo `JobRunStore` (only package that imports `mongodb`) |

Single-graph execution comes from `@exellix/graph-engine`. This package never imports `mongodb`.

> A **JobDef** says *what to run*; a **JobRun** is *one queued execution* of one graph on one item. "Task" stays reserved for graph **task nodes** and **ai-tasks** (`@exellix/ai-tasks`). Full design in [`temp/jobs/`](../temp/jobs/README.md).

## Install

```bash
npm install @exellix/jobs @exellix/jobs-db
```

## Quick start

```ts
import { createJobRunStore } from '@exellix/jobs-db';
import { enqueue, runWorker, createWorkerDeps, createGraphEngineWorkerFns } from '@exellix/jobs';

const { store } = await createJobRunStore({ mongoUri: process.env.MONGO_URI });

// 1. produce job runs from items (one per graph in the job def)
await enqueue(store, [{ id: 'doc-1', data: { question: 'hi' } }], jobDef);

// 2. wire the graph engine and drain the queue
const { compile, executeGraph } = createGraphEngineWorkerFns({ graphLoader });
const deps = createWorkerDeps(store, 'worker-1', { compile, executeGraph });

await runWorker(deps, { concurrency: 4, pollIntervalMs: 60_000, store, signal });
```

For sources, a thin producer loop polls and enqueues:

```ts
import { runSourcePoller } from '@exellix/jobs';
runSourcePoller(store, jobDef, { signal });   // skips when jobDef.enabled === false
```

## Memorix record tracking (EXLX-CRS-001)

Poll Memorix records, enqueue job runs, execute graphs, stamp `_graphRuns`, and write graph results per `response.persistency`:

```bash
cd jobs && npm run build
# Requires MONGO_URI, CATALOX_APP_ID, MEMORIX_* env (see graph-engine/.env.example)
exellix-jobs run --job-def-id=your-job-def-id
```

Commands: `worker` (consume only), `poller` (produce only), `run` (both).

### Activix observability

`exellix-jobs run` / `worker` wire Activix graph/node lifecycle to Mongo (same as `exellix-jobs-api queue-worker`). Set `ACTIVIX_STORAGE_MODE=database` in `.env` — see `jobs/.env.example` and `jobs-api/.env.example`. The Live Console in jobs-ui reads activities from Mongo via jobs-api, not from any local `playground/` folder.

### `_memorixRef` on enqueue

Every Memorix-backed run needs `_memorixRef` on `JobRun.input` so the graph-run tracker can call `@x12i/memorix-writer` (`markGraphRunStarted`, `writeGraphRunResult`).

| Source | How ref is attached |
|--------|---------------------|
| `createMemorixSource` poll | Full ref on item data |
| `enqueue()` | Auto-attaches from memorix-shaped item ids (`entity:…:contentType:recordId`) when absent |
| jobs-ui work factory | On-demand single/batch, continuous evaluate, `/jobs/enqueue` |
| `runWorkFactoryEvaluationTick` | Via centralized `enqueue()` |

Helpers (exported):

```ts
import {
  buildMemorixRecordRef,
  parseMemorixItemId,
  itemDataWithMemorixRef,
  memorixItemId,
  createMemorixGraphRunTracker,
  createDynamicMemorixGraphRunTracker,
  resolveGraphRunContract,
  resolvePersistencyTarget,
} from '@exellix/jobs';

const ref = buildMemorixRecordRef('entity', 'assets', 'core', 'doc-1');
await enqueue(store, [{
  id: memorixItemId(ref),
  data: itemDataWithMemorixRef({ recordId: 'doc-1' }, ref),
}], jobDef);
```

The worker strips `_memorixRef` from graph runtime input; the tracker reads it from the stored `JobRun.input`.

Programmatic wiring:

```ts
import { createMemorixJobsRuntime, runMemorixJobs } from '@exellix/jobs';

await runMemorixJobs({ jobDefId: 'assets-scoped-job', mode: 'both', concurrency: 4 });
```

Live E2E validation:

```bash
npm run bind:memorix-catalox   # once: Catalox bindings for CATALOX_APP_ID
npm run seed:memorix-e2e
npm run check:memorix-e2e
npm run test:memorix-live
```

**Mongo:** replica set gives atomic result+stamp via `@x12i/memorix-writer` transactions. Standalone Mongo is supported — the worker retries with best-effort sequential writes. Set `MEMORIX_GRAPH_RUN_TRANSACTIONS=0` to skip transactions entirely.

## Graph-run persistency (MRX-FRS-002)

Graphs declare writeback intent in `graph.response.persistency` (Graphenix 2.7.3). On successful runs, `createMemorixGraphRunTracker` / `createDynamicMemorixGraphRunTracker` call `writeGraphRunResult` when the graph contract includes persistency and `_memorixRef` is present.

| Export | Role |
|--------|------|
| `resolveGraphRunContract(graphId, doc)` | Read `graph.response.persistency` from graph JSON (re-exported from `@exellix/graph-engine`) |
| `resolvePersistencyTarget(contract, ref)` | Map to `@x12i/memorix-descriptors` `PersistencyTarget` (`newRecord: true` → `{ generate: true }`) |
| `createMemorixGraphRunTracker` | Static contract list (CLI / memorix jobs runtime) |
| `createDynamicMemorixGraphRunTracker` | Load contract per run via `graphLoader` (jobs-ui worker) |

See [docs/handoff/graph-run-persistency-upstream.md](../docs/handoff/graph-run-persistency-upstream.md).

## Processing layer — linked context and associated enrichment

The processing layer resolves **linked object definitions** before graph dispatch. It has two modes that share the same linked-record engine (`resolveLinkedRecordSet` in `context/linked-object-resolution.ts`):

| Mode | When | Output |
|------|------|--------|
| **Runtime context enrichment** | Before a skill/graph run (worker, sync execute, Graph Studio) | Extends injected `jobMemory.context["linked-information"]` — not persisted to source records |
| **Persistent associated enrichment** | Preview / operational writeback (generic product code) | Smart-merges linked record `.data` into `associatedData`, `associatedInferred`, `associatedAnalysis`, or `associated<CustomName>` on source snapshots |

`@exellix/job-dispatcher` does **not** read Memorix or resolve linked definitions. Hosts (`@exellix/jobs`, `@exellix/jobs-api`, Graph Studio BFF) resolve first, then pass `runtime.context.jobMemory` into dispatch.

### Runtime linked-information

Configured via `ContextSource[]` on the job (linked + same-object sources). The worker and `executeGraphJob` call `resolveContextEnrichment` → `mergeContextEnrichment`:

```ts
import { resolveJobMemoryForDispatch, mergeContextEnrichment } from '@exellix/jobs';

const jobMemory = await resolveJobMemoryForDispatch(retrieval, runtimeInput, contextSources, {
  sourceEntity: 'assets',
  existingJobMemory: injectedJobMemory,
  recordId: 'asset-1',
});
// jobMemory.context['linked-information'] → [{ context: '...', data: [...] }]
```

Output rules (locked by tests):

- Linked items contain only `{ context, data }` — no resolution metadata in `jobMemory`.
- Empty linked results do not create `linked-information` items or an empty `context` object.
- Existing `jobMemory.context` from other providers is preserved.

### Sync execute and Graph Studio

`executeGraphJob` accepts optional `contextSources`, `sourceEntity`, and `recordId`. When `contextSources` is set, `options.retrieval` is required; resolution runs before `dispatchGraphRun`:

```ts
import { executeGraphJob } from '@exellix/jobs';

await executeGraphJob(
  {
    graphId: 'cyber-analysis.assets.extended',
    input: assembledInput,
    contextSources: [
      {
        kind: 'linked',
        objectType: 'subnets',
        contentType: 'inferences',
        linkingProperty: 'data.subnetIp',
        targetLinkingProperty: 'data.subnetIp',
        matchContentType: 'snapshots',
        mandatory: true,
      },
    ],
    sourceEntity: 'assets',
    recordId: 'asset-1',
  },
  { graphLoader, retrieval },
);
```

`buildContextProbeRun` builds a synthetic `JobRun` for read-only probes (preview, coverage, pre-dispatch resolution).

### Persistent associated enrichment (preview / write helpers)

Generic config (`AssociatedEnrichmentConfig`): `source`, `linked`, `output`, optional `filter` and `failure`. Preview resolves and smart-merges without writing:

```ts
import { previewAssociatedEnrichment, suggestAssociatedTargetProperty } from '@exellix/jobs';

suggestAssociatedTargetProperty('inferences'); // → 'associatedInferred'

const preview = await previewAssociatedEnrichment(retrieval, {
  recordId: 'asset-1',
  sourceInput: { recordId: 'asset-1', data: { subnetIp: '10.0.0.0/24' } },
  existingAssociatedValue: [{ riskLevel: 'medium' }],
  config: {
    source: { objectType: 'assets', contentType: 'snapshots' },
    linked: {
      objectType: 'subnets',
      contentType: 'inferences',
      linkingProperty: 'data.subnetIp',
      targetLinkingProperty: 'data.subnetIp',
      matchContentType: 'snapshots',
    },
    output: { mode: 'persist-to-source', targetProperty: 'associatedInferred', writeMode: 'smart-merge' },
  },
});
// preview.ok, preview.merge, preview.willWrite — or preview.failure for mandatory misses
```

Smart merge: preserve existing arrays, normalize single objects to arrays, append only canonically unique objects, skip empty writes. Mandatory failures are modeled as generic `<objectType>-failed` records (`buildAssociatedEnrichmentFailure`).

### Preflight — input coverage

`sampleGraphInputCoverage` samples batch record ids and reports per-record assembly gaps plus per-context-source coverage (resolved counts, filter/mandatory failure reasons):

```ts
import { sampleGraphInputCoverage } from '@exellix/jobs';

const coverage = await sampleGraphInputCoverage({
  retrieval,
  graph,
  selection,
  recordIds,
  contextSources,
  sourceEntity: 'assets',
  sampleSize: 20,
});
```

HTTP equivalents live on `@exellix/jobs-api` (`POST /api/graph/input-coverage`, `POST /api/context/preview`, `POST /api/associated-enrichment/preview`).

### Associated properties on snapshots (`associated*`)

Snapshot records can carry **associated\*** root properties that hold linked or enriched data accumulated over time. Naming follows the linked content type:

| Linked content type | Property on source snapshot |
|---------------------|----------------------------|
| `snapshots` | `associatedData` |
| `inferences` | `associatedInferred` |
| `analysis` | `associatedAnalysis` |
| custom | `associated<CustomName>` |

**At execution time**, assembly promotes snapshot root fields whose names start with `associated` out of `runtime.input` and onto direct `jobMemory.associated*` fields (for example `jobMemory.associatedData` or `jobMemory.associatedRiskScores`). `promotedProperties` can also opt explicit top-level fields into the same job-memory path. Skills should read enriched associated data from `jobMemory`, not from flat graph input.

**Persistent associated enrichment** (`previewAssociatedEnrichment`) resolves linked records and smart-merges their `.data` into the configured `associated*` array on the source snapshot (preview-only in product code; applying to real records is a separate operational step). Smart merge preserves existing arrays, normalizes single objects to arrays, appends only canonically unique objects, and skips empty writes.

## Memorix integration (via graph-engine)

`@exellix/jobs` does **not** import Memorix directly. When job graphs use local skills such as `scoped-data-reader` or `scoped-answer-writer`, Memorix I/O happens inside `@exellix/graph-engine` at execute time.

Wire the worker the same way as above — `createGraphEngineWorkerFns` passes runtime options into graph-engine, which lazily initializes Memorix clients from process env:

| Package | Version | Role |
|---------|---------|------|
| `@x12i/memorix-descriptors` | 1.10.0 | `PersistencyTarget`, `PersistencyLinkIntent` |
| `@x12i/memorix-retrieval` | 1.14.0 | `scoped-data-reader` reads, list/poll |
| `@x12i/memorix-writer` | 1.3.0 | `_graphRuns` stamp + MRX-FRS-002 `writeGraphRunResult` |

**Required host env** (see [`graph-engine/.env.example`](../graph-engine/.env.example)):

- `MONGO_URI` — job runs and Memorix stores
- `MEMORIX_ENTITIES_DB`, `MEMORIX_EVENTS_DB`
- `CATALOX_APP_ID` (and related Catalox config for descriptor lookup)
- LLM keys when graphs include ai-tasks nodes

On shutdown, close the job store and release Memorix clients:

```ts
import { shutdownMemorixRuntime } from '@exellix/graph-engine';

signal.addEventListener('abort', async () => {
  await runWorkerPromise; // or abort runWorker via shared signal
  await store.close();
  await shutdownMemorixRuntime();
});
```

## API

| Function | Role |
|----------|------|
| `enqueue(store, items, jobDef)` | items → `JobRun[]`; auto-attaches `_memorixRef` from memorix item ids |
| `buildMemorixRecordRef` / `parseMemorixItemId` / `itemDataWithMemorixRef` | Memorix ref helpers for producers |
| `createMemorixGraphRunTracker` / `createDynamicMemorixGraphRunTracker` | Post-run `_graphRuns` + persistency writeback |
| `resolveGraphRunContract` / `resolvePersistencyTarget` | Graph persistency → writer target |
| `claim(store, workerId, opts?)` | atomic claim → `JobRun \| null` |
| `complete(store, jobRunId, result)` | mark done; release dependents (merging upstream output) |
| `fail(store, jobRunId, error)` | retry with backoff while attempts remain, else terminal-fail + cascade |
| `releaseDependents` / `mergeUpstream` | dependency release and upstream-output-as-input |
| `sweepExpiredLeases(store, { leaseMs })` | reclaim job runs from dead workers |
| `runWorker(deps, opts)` | the only loop (concurrency, idle sleep, periodic sweep) |
| `drainWorker(deps, opts)` | run once until the queue is empty (tests / batch) |
| `createGraphEngineWorkerFns(options)` | wire `@exellix/graph-engine` compile + execute |
| `runQueueAdmissionLoop` / `promoteWaitingRuns` | app-wide admission — promote held runs when slots free up |
| `runWorkFactoryEvaluationTick` | continuous work poller (active work defs → enqueue eligible records) |
| `resolveJobMemoryForDispatch` | Host-facing pre-dispatch context resolution → `jobMemory` for dispatch |
| `resolveContextEnrichment` / `mergeContextEnrichment` | Runtime linked/same-object context → `linked-information` / `extendedInformation` |
| `resolveLinkedRecordSet` | Shared linked-record fetch, join, filter engine (runtime + associated modes) |
| `previewAssociatedEnrichment` | Preview persistent associated* smart-merge for one source record (no write) |
| `sampleGraphInputCoverage` | Batch preflight: assembly + per-context-source coverage sample |
| `executeGraphJob` | Sync graph run; optional `contextSources` + `retrieval` for pre-dispatch resolution |

## Parallel execution and capacity

Exellix runs **many graph executions concurrently**, but that does **not** mean one OS process per job. A typical deployment is **one `exellix-jobs` Node process** with an in-memory async pool; admission control decides how many runs may be active app-wide.

### Two layers of concurrency

| Layer | What it limits | Default / config |
|-------|----------------|------------------|
| **App admission** | How many job runs are admitted (pending + running, claimable) across all work, batches, and on-demand runs | `maxConcurrentJobs` in Mongo `jobs_app_settings` — **10** ([`DEFAULT_JOBS_APP_SETTINGS`](../jobs-db/src/app-settings-store.ts)). Edit in [jobs-ui → Settings](../jobs-ui/README.md). |
| **Worker pool** | How many runs one worker process executes at once | `runWorker({ concurrency })`, `--concurrency`, or `JOBS_WORKER_CONCURRENCY` (falls back to `maxConcurrentJobs`) |
| **Graph nodes** | Parallel task nodes *within one graph run* | `WOREX_GRAPH_CONCURRENCY` env or graph metadata — **4** per runnable batch ([graph-engine](../graph-engine/.docs/environment-and-xmemory-databases.md)) |

When admission is full, extra runs are held (`availableAt` sentinel) until a slot frees; the admission loop promotes them on `queueAdmissionIntervalMs` (default **15s**).

```
enqueue → admission (maxConcurrentJobs) → claim → runOne → executeGraph
                                              ↑
                         promoteWaitingRuns when a run completes
```

### Environment

| Variable | Role |
|----------|------|
| `JOBS_WORKER_CONCURRENCY` | Override worker pool size for this process |
| `JOBS_POLL_INTERVAL_MS` | Idle poll when no claimable runs |
| `JOBS_LEASE_MS` | Claim lease; expired leases are swept back to pending |
| `WOREX_GRAPH_CONCURRENCY` | Cap parallel nodes inside each graph (lower this when running many admitted jobs) |

CLI example — 20 admitted jobs but only 2 parallel nodes per graph:

```bash
JOBS_WORKER_CONCURRENCY=20 WOREX_GRAPH_CONCURRENCY=2 exellix-jobs run --job-def-id=your-job-def-id
```

### Sizing guidance

There is **no hard upper cap** on `maxConcurrentJobs` in code — capacity is bounded by the host and downstream services.

| Target | Typical host | Notes |
|--------|--------------|-------|
| **8–12** | Dev laptop (8 cores, 16 GB RAM) | Comfortable default when Cursor, Mongo, and jobs-ui also run locally |
| **10** | Any | Shipped default — good balance for I/O-bound graphs (Mongo, LLM APIs) |
| **20** | 16 GB+ RAM, dedicated worker | Workable if graphs are mostly waiting on network; watch RAM and Mongo connection pools |
| **50–100+** | Dedicated worker fleet (32–64 GB+ RAM) | Run **multiple worker processes** (distinct `WORKER_ID`s), each with modest `JOBS_WORKER_CONCURRENCY` (8–16); not one Node process on a dev machine |

**Bottlenecks to watch:** Node heap (one process holding many in-flight graphs), MongoDB pool/contention, LLM provider rate limits, and lease expiry if the event loop stalls under load.

For high throughput, scale **workers horizontally** rather than pushing a single process to 100 concurrent runs.

## What it adds over the Execution Matrix

- **Automatic retry with backoff** — `maxAttempts` + `availableAt`.
- **Cross-item dependencies** — a job run may depend on a job run from a different item.
- **Upstream-output-as-input in the queue layer** — `mergeUpstream`, out of engine internals.

## Scripts

```bash
npm run build      # tsc → dist/
npm test           # unit + live (live runs only when MONGO_URI is set)
npm run test:live  # live Mongo integration tests only
```

## License

exellix-license

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