Skip to content

Anti-Patterns

These patterns break Verist's guarantees. Avoid them.

Hidden state in steps

Don't

ts
let cache = {}; // module-level state

const step = defineStep({
  run: async (input, ctx) => {
    if (cache[input.id]) return cache[input.id]; 
    const result = await ctx.adapters.llm.run(input);
    cache[input.id] = result;
    return { output: result };
  },
});

Do

Pass all required data through input or adapters. State lives in the database, not in memory.

Why it breaks: Replay and recompute assume the step has no memory between runs. Hidden state makes outputs non-reproducible.

Writing to the database inside steps

Don't

ts
const step = defineStep({
  run: async (input, ctx) => {
    await db.insert({ id: input.id, status: "processed" }); 
    return { output: { processed: true } };
  },
});

Do

Return an output. Let your runner commit it atomically after the step completes.

Why it breaks: If the step fails after the write, you have partial state. Replay becomes impossible.

Retries inside steps

Don't

ts
const step = defineStep({
  run: async (input, ctx) => {
    for (let i = 0; i < 3; i++) {
      try {
        return await riskyCall(input);
      } catch (e) {
        continue;
      }
    }
    throw new Error("Failed after retries");
  },
});

Do

Let the runner handle retries. Steps should fail fast and return errors as values.

Why it breaks: Internal retries hide failure modes. The audit trail shows success even though three attempts happened.

Side effects in steps

Don't

ts
const step = defineStep({
  run: async (input, ctx) => {
    await sendEmail(input.userId, "Your request was processed"); 
    return { output: { notified: true } };
  },
});

Do

Return an emit command. Let your runner send the email after the step commits.

Why it breaks: If the step is replayed or recomputed, the email is sent again. Side effects should be explicit commands interpreted by your runner.

Ignoring commands

Don't

ts
const result = await run(step, input, ctx);
if (result.ok) {
  await store.commit(result.value.output);
  // commands silently dropped
}

Do

Interpret every command. Use your queue for invoke, your review system for review, your event bus for emit.

Why it breaks: Commands represent intended control flow. Dropping them means the workflow stalls silently.

Mixing computed and overlay writes

Don't

ts
const step = defineStep({
  run: async (input, ctx) => {
    await store.writeOverlay({ score: 0.95 }); 
    return { output: { score: 0.85 } };
  },
});

Do

Steps write to computed (via output). Only your review UI writes to overlay.

Why it breaks: The overlay is for human overrides. If steps write to it, human decisions can be silently overwritten.

Silent recompute

Don't

ts
const result = await recompute(snapshot, step, newCtx);
if (result.ok) {
  await store.commit(result.value.output); 
}

Do

Show the diff to a reviewer. Only persist after explicit approval.

Why it breaks: Recompute is for seeing what would change. Persisting without review defeats the purpose.

Capturing too little

Don't

ts
const result = await run(step, input, {
  adapters,
  // no onArtifact callback
});

Do

Capture all non-deterministic inputs/outputs via onArtifact. Store them with your snapshot.

Why it breaks: Replay only works if you have all the artifacts. Missing artifacts mean approximate replay at best.

Summary

Anti-patternConsequence
Hidden stateNon-reproducible outputs
DB writes in stepsPartial state on failure
Retries in stepsHidden failure modes
Side effects in stepsDuplicate effects on replay
Ignoring commandsSilent workflow stalls
Steps writing overlayHuman overrides lost
Silent recomputeUnreviewed changes
Missing artifactsApproximate replay

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