Little documentation
Guides and APIInternals

Identity Manifests

WorkflowDefinitionHash, the three harness manifests, drift detection on resume, and how identity links across a pipeline run.

Little Workflow: published on npm as little-workflow@0.2.0-alpha.1 (2026-10-06). Sections labeled UNRELEASED describe source-only changes. Release status

Little Workflow uses content-addressed identity at every layer. This page covers the two workflow-level hashes and the three harness session manifests that compose them for drift detection and audit.

Two Workflow Hashes

WorkflowDefinitionHash

A stable hash of the workflow's identity-bearing fields at authoring time. Computed once when defineWorkflow({...}) is called; cached on the definition.

type WorkflowDefinitionSnapshot = {
  readonly id: string;
  readonly description: string;
  readonly inputSchemaHash: string;
  readonly outputSchemaHash: string;
  readonly modelSlots: readonly {
    readonly id: string;
    readonly providerId: string;
    readonly modelId: string;
    readonly description: string;
  }[];                                           // sorted by slot id
  readonly plannerConfig: {
    readonly modelSlotId: string;
    readonly harnessId: string;
    readonly systemHash: string;
    readonly skillsHash: string;
  };
  readonly workerConfig: {
    readonly harnessId: string;
    readonly skillsHash: string;
  };
  readonly globalTools: readonly string[];       // sorted; tool ids referenced
  readonly memoryConfig: {
    readonly workflow: "rw" | "ro" | "none";
    readonly org: "rw" | "ro" | "none";
    readonly attached: readonly { id: string; mode: "ro" }[];  // sorted by id
  };
  readonly bashConfig: {
    readonly network: false | {
      readonly allowedUrlPrefixes?: readonly string[];
      readonly allowedMethods?: readonly string[];
      readonly denyPrivateRanges: boolean;
      readonly maxRedirects?: number;
      readonly timeoutMs?: number;
      readonly maxResponseSize?: number;
    };
    readonly python: boolean;
    readonly javascript: boolean;
  };
};

WorkflowDefinitionHash = sha256(canonicalJson(WorkflowDefinitionSnapshot));

Used by: orchestrator manifest (available workflow list), planner manifest (pins to one definition), little workflows list CLI.

WorkflowVersion

Unchanged: sha256(canonicalJson(LWIR)). Computed per planner draft. Two compiles that produce byte-identical LWIR collapse to the same WorkflowVersion.

Relationship

WorkflowDefinition  (defineWorkflow)
        ↓ hash once
WorkflowDefinitionHash  (stable)
        ↓ run with input
LWIR  (per planner draft)
        ↓ hash
WorkflowVersion  (per run)
        ↓ execute
RunCompleted  (with output)

A WorkflowDefinition produces many WorkflowVersions. The definition hash is stable across runs as long as the definition doesn't change; the version varies with input and planner non-determinism.

The Three Manifests

Every harness session stores a manifest in its harness.session.started event. The manifest describes the capability surface the session was given. On resume, the SDK recomputes the manifest and compares hashes — mismatch fails closed.

OrchestratorManifest

For orchestrate tasks.

type OrchestratorManifest = {
  readonly harnessId: string;                    // e.g. "workflowHarness@1.0.0"
  readonly orchestratorModelSlotId: string;
  readonly systemPromptHash: string;
  readonly skillsHash: string;
  readonly availableWorkflows: ReadonlyArray<{
    readonly id: string;
    readonly definitionHash: string;
  }>;                                            // sorted by id
  readonly globalToolsHash: string;
  readonly memoryStoreIds: ReadonlyArray<string>; // sorted
  readonly bashCapabilitiesHash: string;
  readonly maxConcurrentSubRuns: number;
};

OrchestratorManifestHash = sha256(canonicalJson(OrchestratorManifest));

PlannerManifest

For plan tasks, whether top-level or nested inside an orchestrator.

type PlannerManifest = {
  readonly harnessId: string;
  readonly plannerModelSlotId: string;
  readonly systemPromptHash: string;
  readonly skillsHash: string;
  readonly workflowDefinitionHash: string;
  readonly globalToolsHash: string;
  readonly memoryStoreIds: ReadonlyArray<string>;
  readonly bashCapabilitiesHash: string;
  readonly outerLoopContextHash?: string;        // present iff this is a re-plan after supervise
};

PlannerManifestHash = sha256(canonicalJson(PlannerManifest));

WorkerManifest

For execute_step tasks, one per step execution.

type WorkerManifest = {
  readonly harnessId: string;
  readonly workflowDefinitionHash: string;
  readonly workflowVersionId: string;
  readonly stepPath: string;                     // e.g. "summarize" or "candidates[3].review"
  readonly stepConfigHash: string;               // sha256 of the full LWIR step config
  readonly modelSlotId?: string;                 // only for ai.generate steps
  readonly systemPromptHash?: string;            // only for ai.generate
  readonly skillsHash: string;
  readonly allowedToolsHash: string;
  readonly memoryStoreIds: ReadonlyArray<string>;
  readonly bashCapabilitiesHash: string;
};

WorkerManifestHash = sha256(canonicalJson(WorkerManifest));

For direct-execute steps (code.run, tool.call, decision), modelSlotId and systemPromptHash are absent.

FixerManifest

For fix_step tasks. Extends WorkerManifest with fixer-specific fields:

type FixerManifest = WorkerManifest & {
  readonly fixerModelSlotId: string;
  readonly fixerSystemHash: string;              // sha256 of onFailure.fixer.system
  readonly fixerMaxAttempts: number;
};

harness.session.started Event Payload

{
  "type": "harness.session.started",
  "payload": {
    "runId": "run_abc123",
    "role": "orchestrator",
    "task": { "kind": "orchestrate" },
    "parentRunId": null,

    "manifest": {
      "harnessId": "workflowHarness@1.0.0",
      "orchestratorModelSlotId": "gpt-5",
      "systemPromptHash": "sha256:...",
      "skillsHash": "sha256:...",
      "availableWorkflows": [
        { "id": "candidate.review", "definitionHash": "sha256:def123" },
        { "id": "candidate.scoring", "definitionHash": "sha256:def456" }
      ],
      "globalToolsHash": "sha256:...",
      "memoryStoreIds": ["pipeline:abc", "org"],
      "bashCapabilitiesHash": "sha256:...",
      "maxConcurrentSubRuns": 10
    },
    "manifestHash": "sha256:orch789"
  }
}

The manifest is inlined (not just hashed) so little events <runId> --type harness.session.started shows exactly what config the session ran against. Manifest size is ~500–2000 bytes per session — negligible.

Drift Detection on Resume

async function checkManifestDrift(world, runId, currentManifest) {
  const events = await world.events.read(runId);
  const startedEvent = events.find(e => e.type === "harness.session.started");
  if (!startedEvent) return;  // fresh run

  const storedHash = startedEvent.payload.manifestHash;
  const currentHash = sha256(canonicalJson(currentManifest));

  if (storedHash !== currentHash) {
    throw new CapabilityDriftError({
      runId,
      storedManifest: startedEvent.payload.manifest,
      currentManifest,
      diff: computeManifestDiff(startedEvent.payload.manifest, currentManifest),
    });
  }
}

CapabilityDriftError is thrown BEFORE any execution resumes. The dev sees what changed:

CapabilityDriftError: Run 'run_abc123' was started with a different orchestrator config.

Changes detected:
- orchestratorModelSlotId: "gpt-5" → "gpt-4o-mini"
- availableWorkflows[1].definitionHash: "sha256:def456" → "sha256:def789"
  (candidate.scoring workflow definition changed)

To resume cleanly, restore the prior state.
To start fresh, run with a new runId.

What counts as drift (orchestrator session)

ChangeDrift?
orchestrator.model slot changedYes
orchestrator.system prompt text changedYes
orchestrator.harness changedYes
Order or set of workflows: [...] changedYes
Any sub-workflow's WorkflowDefinitionHash changedYes
Tool registry gained/lost a tool or schema changedYes
Bash sandbox/network capability changedYes
Org memory store contents changedNo — only store IDs are in the manifest
Skill body changed but frontmatter unchangedNo — only frontmatter is hashed
Skill added or removedYes — changes skillsHash

Dev options on drift

  • Re-run fresh — discard the runId; create a new run.
  • Restore prior state — revert the definition or tool registry changes and resume cleanly.
  • acceptManifestDrift: true is post-alpha.

Manifest Composition in a Pipeline Run

A pipeline run nests multiple harness sessions:

pipeline_run_X  (orchestrator session — OrchestratorManifest)
  plan session for candidate.review  (PlannerManifest — workflowDefinitionHash: "sha256:def123")
  run session for candidate.review × 50
    each sub-run: worker sessions per LWIR step  (WorkerManifest — workflowVersionId: "sha256:lwir1")
  plan session for candidate.scoring  (PlannerManifest — workflowDefinitionHash: "sha256:def456")
  run session for candidate.scoring  (WorkerManifest per step)

Identity links from a worker step back to the orchestrator:

WorkerManifest.workflowVersionId  →  LWIR hash
                                      ↓ emitted by planner with
PlannerManifest.workflowDefinitionHash
                                      ↓ appears in
OrchestratorManifest.availableWorkflows[X].definitionHash
                                      ↓ hashed in
harness.session.started of pipeline_run_X

All edges are durable in the event log. The CLI can render the dependency chain for any committed step.

Re-plan After Supervise

When a planner re-plans after supervise() → continue:

  • The new session receives outerLoopContextHash (sha256 of prior cycles) in its PlannerManifest.
  • The new PlannerManifestHash differs from the prior planner manifest — each cycle has its own manifest.
  • New LWIR may yield a different WorkflowVersion; subsequent worker sessions reference the new version.

WorkflowDefinitionHash in Other Contexts

UseDetail
CLI inspectionlittle workflows list shows {id, hash, description}
Cross-machine compatibilitySame definition → same hash → interchangeable events across machines sharing <dataDir>
Memory keyWorkflow memory path is <dataDir>/memory/workflows/<workflowId>/ — id-keyed not hash-keyed. Memory persists across definition changes; drift detection still works because the manifest pins the hash at run time.
  • Harness — where harness.session.started is emitted
  • Memory + Scratch — how memoryStoreIds are constructed
  • Fixer — FixerManifest shape
  • Event Log — the underlying SQLite/WAL store that holds manifest events

On this page