Skip to content

SPEC: Overview ​

Concepts ​

Workflow – Named sequence of steps with typed state.

Step – Pure function: (input, context) => { output, events?, commands? }. Atomic, idempotent. May call LLMs. Commands express routing declaratively.

State – Domain data persisted between steps. Source of truth is database.

Event – Audit record emitted by steps. Immutable log of what happened and why.

Context – Runtime dependencies (db, llm, queue) injected into steps.

Flow ​

Queue message → Load state → Run step → Persist output + events → Emit next message

Steps don't know about queues. Orchestration is external. The kernel is compatible with short-lived, stateless execution environments (edge/serverless) by design.

Kernel Invariants ​

Step Invariants ​

Steps are the atomic unit of the kernel. These rules are non-negotiable:

  • Idempotent: Same input + state → same output. Safe to retry.
  • Pure: No global mutable state. All dependencies injected via context.
  • Explicit effects: Side effects expressed only via events and commands.
  • Replay-safe: Replayable given only recorded input, state, and artifacts.
  • Self-contained: No implicit dependencies on execution order or external state.

Any value that influences a step's output must be input, state, or artifact-addressable.

Serialization Contract ​

Kernel serialization (stableStringify, hashValue) follows JSON semantics for undefined normalization. This applies to all hashing and artifact identity in Verist:

  • Top-level undefined becomes null
  • Object properties with undefined values are omitted
  • undefined in arrays becomes null

This ensures deterministic hashing regardless of how values were constructed.

API Sketch ​

typescript
const workflow = defineWorkflow({
  name: "verify-document",
  version: "1.0.0", // required for audit
  steps: { extract, verify, score },
});

const step = defineStep({
  name: "extract",
  input: z.object({ documentId: z.string() }),
  output: z.object({ claims: z.array(ClaimSchema) }),
  run: async (input, ctx) => {
    const doc = await ctx.adapters.db.getDocument(input.documentId);
    const claims = await ctx.adapters.llm.extract(doc.content);
    return {
      output: { claims },
      events: [{ type: "claims_extracted", payload: { count: claims.length } }],
      // Type-safe: workflow.invoke infers input type from step schema
      commands: [workflow.invoke("verify", { claims })],
    };
  },
});

Expected failures return values instead of throwing. Use fail() to preserve structured error metadata for runners (see ADR-012 and SPEC-steps):

typescript
import { fail } from "verist";

const result = await someAdapterCall();
if (!result.ok) return fail(result.error);

Commands ​

Steps return optional commands to express "what should happen next" declaratively:

typescript
type Command =
  | { type: "invoke"; step: string; input: unknown } // request next step
  | { type: "fanout"; step: string; inputs: unknown[] } // parallel processing
  | { type: "review"; reason: string; payload?: unknown } // human-in-the-loop
  | { type: "emit"; topic: string; payload: unknown } // external event
  | {
      type: "suspend";
      reason: string;
      checkpoint: unknown;
      resumeStep?: string;
    }; // await external input

Commands are data, not execution. The external runner interprets them.

Command Semantics ​

  • invoke / fanout: Control commands. These direct execution to other steps. fanout inputs are logically independent; each input represents an isolated step execution. Runners may batch or parallelize, but must not share mutable state between executions.

  • review: Blocking command. Workflow progression must stop until an external decision is provided. How the decision is captured and how execution resumes are runner concerns.

  • suspend: Blocking command. Pauses workflow until external data arrives. Unlike review (human approval), suspend awaits data/callbacks. Sibling commands are discarded; the resumed step emits new commands. See SPEC-suspend.

  • emit: Side-effect command for external systems. Distinct from audit events; not part of the internal evidence log.

Audit Event Structure ​

typescript
// Core emits minimal events; orchestrator adds id, timestamp, workflowId, stepName
interface AuditEvent {
  type: string;
  payload?: Record<string, unknown>;
  llmTrace?: LLMTrace;
}

interface LLMTrace {
  model: string;
  promptTokens: number;
  completionTokens: number;
  durationMs: number;
  inputHash: string; // always present for audit correlation
  outputHash: string; // always present for deduplication
  input?: unknown; // optional - can be omitted for compliance
  output?: unknown; // optional - can be omitted for compliance
}

State Management ​

  • Computed: AI-derived, rewritten on recompute
  • Overlay: Human overrides, never touched by recomputation
  • Effective: { ...computed, ...overlay } – overlay keys take precedence

Recomputation never modifies human decisions. See ADR-003 for merge semantics.

Execution Contract ​

After calling run(), the caller must complete the following to honor the kernel's guarantees. Below, stepResult refers to the unwrapped StepResult (i.e., result.value when result.ok === true).

  1. Persist the output – Merge stepResult.output into storage (computed layer). Without this, state is lost.

  2. Record events – Write stepResult.events to your audit log. Events are the evidence trail.

  3. Enqueue commands – If stepResult.commands is non-empty, translate each command to your queue/orchestration system.

typescript
const result = await run(step, input, {
  adapters,
  workflowId: "verify-document",
  workflowVersion: "1.0.0",
  runId: crypto.randomUUID(),
});

if (result.ok) {
  // 1. Persist output
  await store.commit({
    workflowId: result.value.workflowId,
    runId: result.value.runId,
    stepId: result.value.stepName,
    expectedVersion: currentVersion,
    output: result.value.output,
    events: result.value.events,
  });

  // 2. Enqueue commands
  for (const cmd of result.value.commands ?? []) {
    await queue.enqueue(cmd);
  }
}

Verist guarantees correctness if and only if this contract is honored.

Skipping any step breaks invariants:

  • Skipping persistence → state drift, failed replays
  • Skipping events → incomplete audit trail
  • Skipping commands → workflow stalls

Kernel Boundaries ​

Verist is a kernel, not a platform. The following are explicitly external:

Orchestration: The kernel produces commands; external runners execute them. Verist does not manage queues, workers, or retry policies.

Approval workflows: When a step returns a review command, the kernel's job is done. Approval UIs, escalation logic, and persistence of approvals are external.

Diff review and persistence: recompute() produces a diff. The decision to accept, reject, or modify that diff – and persist the outcome – is external.

The kernel's contract ends at: (input, artifacts) → (output, events, commands).

Runner constraints: External orchestrators must treat steps as pure black boxes:

  • Must not mutate state directly
  • Must not interpret step output beyond commands
  • Must not inject implicit retries or branching logic
  • Must not add behavior not expressed in commands

Runtime assumptions: Runners may be short-lived and stateless. Steps must not depend on:

  • In-memory durable state across executions
  • Background loops or long-lived workers
  • Local filesystem for persistence

Layer boundaries: Higher-level packages consume only the kernel's public outputs – audit events and step outputs – never raw internal state. This ensures the kernel remains universal and extensions are purely additive.

Future: Trust Kit ​

The Trust Kit is a planned Tier 2 capability package (@verist/trust) that will provide opt-in primitives for high-trust workflows:

  • Evidence tiers and verdict types
  • Contradiction detection and review escalation
  • Computed/overlay helpers with conflict surfacing
  • Immutable snapshot builders

The kernel (verist) remains universal; Trust Kit adds domain-specific trust primitives.

LLM context: llms.txt · llms-full.txt
Released under the Apache 2.0 License.