Skip to content
Français

Reuse task results

Cache JSON outputs when a task can safely reuse a result for the same inputs.

Add a cache policy when a task can reuse the same JSON result for the same inputs. Give the policy a store, a version and a key that identifies the inputs affecting the result.

import { mkdtemp } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
  createTaskCacheStore,
  createLocalTransport,
} from "@elie-laloum/outpost";

export const directory = await mkdtemp(join(tmpdir(), "outpost-cache-"));
export const store = createTaskCacheStore({
  transporter: createLocalTransport({ directory }),
});
import { defineTask } from "@elie-laloum/outpost";
import { store } from "./cache-store.ts";

export let executions = 0;
export const summarize = () =>
  defineTask({
    key: "summary",
    cache: { store, version: "summary-v1", key: () => ["notes", "v7.1"] },
    perform: () => ({ summary: "3 fixes", execution: ++executions }),
  });
export function executionCount() {
  return executions;
}
import { reportValue } from "./reporter.ts";
import { summarize } from "./summarize.ts";
import { defineWorkflow } from "@elie-laloum/outpost";

for (let run = 1; run <= 2; run++) {
  const summary = summarize();
  const result = await defineWorkflow("release-notes", [summary]).start();
  result.unwrap();
  reportValue(result.value(summary), result.tasks[0]?.cacheHit ?? false);
  // Example output (second run): { summary: '3 fixes', execution: 1 } true
}

The second run restores the first result: execution stays at 1 and cacheHit is true.

The fingerprint combines the workflow name, the task key, version and the JSON value key(ctx) returns. Put in it everything that can change the answer.

API reference: TaskCacheOptions and TaskCacheEntry.

import {
  createTaskCacheStore,
  createLocalTransport,
} from "@elie-laloum/outpost";
import { repository } from "./outpost.config.ts";

export const brief =
  "Review the parser for unsafe input handling. Do not edit files.";
export const store = createTaskCacheStore({
  transporter: createLocalTransport({
    directory: `${repository}/.outpost/storage`,
  }),
});
import { defineIsolatedTask } from "@elie-laloum/outpost";
import { repository, sandboxProvider, coder } from "./outpost.config.ts";
import { brief } from "./review-cache.ts";

export const reviewer = defineIsolatedTask({
  key: "reviewer",
  request: () => ({
    repository,
    sandboxProvider,
    agent: coder,
    brief: { text: brief },
  }),
});
import { defineTask, repositoryFingerprint } from "@elie-laloum/outpost";
import { store, brief } from "./review-cache.ts";
import { repository } from "./outpost.config.ts";
import { reviewer } from "./review-task.ts";

export const review = defineTask({
  key: "review",
  cache: {
    store,
    version: "review-v1",
    key: async () => [await repositoryFingerprint(repository), brief, "codex"],
  },
  perform: async (ctx) => {
    const { text } = await reviewer.perform(ctx);
    return { text };
  },
});

repositoryFingerprint() hashes HEAD, the index, uncommitted changes and non-ignored untracked files outside .outpost/, so a local edit changes the key. A key that throws or is not lossless JSON fails the task.

On a hitResult
Task valueRestored and passed to dependent tasks
TaskRecord.cacheHittrue
Attempts, usage, attempt budgetNone recorded or consumed
Files, commits, branches, sandbox state, artifacts, callsNot replayed

Cache tasks whose value is the product: reviews, classifications, summaries, analyses.

The result must be lossless JSON or undefined; otherwise the task fails after it executes, without a retry.

API reference: TaskCacheOptions, TaskOptions and QueuedTaskOptions.

API reference: TaskCacheOptions.

Log cache outcomes from the workflow observer to see hits, misses and storage errors. A cache failure does not prevent the task from running or completing.

import { reportValue } from "./reporter.ts";
import type { Workflow } from "@elie-laloum/outpost";

declare const workflow: Workflow;

await workflow.start({
  observe: (event) => {
    if (event.type === "cache")
      reportValue(event.key, event.cache, event.error);
    // Example output: summary hit undefined
  },
});

API reference: TaskCacheOutcome.

The cache never decides the outcome: after a failed read the task runs, after a failed write it completes normally.

Entries live under task-cache/ in the transport until you remove them. Add the task-cache scope to a retention policy to prune those older than minAgeMs.

  • Concurrent executions with the same fingerprint all run; the first entry written is kept.
  • A task already completed in a checkpoint is restored from it without reading the cache.
  • Entries are not invalidated when your code or agent changes: change version.
  • createTaskCacheStore does not store entries above 16 MiB (maxBytes); the write reports failed.
  • Do not cache a defineArtifactTask read by a later task: a hit in a new run restores a reference to the earlier run, and readArtifact() fails with “Artifact dependency producer mismatch”.

API: TaskCacheOptions · createTaskCacheStore · repositoryFingerprint · TaskCacheEntry · WorkflowEvent.