Skip to content
Français

Choose where data is stored

Configure transports for durable data and locate the files that stay in the repository.

A transport stores versioned bytes under keys. The stores built on it interpret those bytes as checkpoints, artifacts or other durable data. Choose the transport to decide where the data lives, and the store to decide what it represents.

ObjectWritten throughWithout a transport you pass
CheckpointscreateWorkflowCheckpointStore({ transporter })Required
ArtifactscreateArtifactStore({ transporter })Required
Task cachecreateTaskCacheStore({ transporter })Required
Durable speculationdurability.transporter on speculate()Required
Journalslogging.transporter on a dispatch or sandbox.outpost/storage
Resource activityactivityTransport on a dispatch or sandbox.outpost/storage
Storage reservationstransporter on reserveRecoveryStorage().outpost/storage
Archived conversationscreateTransportConversations(store, { transporter })Not archived
Recovery archivesrecoveryTransport on a dispatch or sandboxNot archived

createLocalTransport({ directory }) stores objects in a private local directory. Pass the repository’s .outpost/storage to keep your stores next to the journals and activity Outpost writes there by default.

import {
  createArtifactStore,
  createLocalTransport,
  createTaskCacheStore,
  createWorkflowCheckpointStore,
} from "@elie-laloum/outpost";

const transporter = createLocalTransport({ directory: ".outpost/storage" });
const checkpoints = createWorkflowCheckpointStore({ transporter });
const artifacts = createArtifactStore({ transporter });
const cache = createTaskCacheStore({ transporter });

Creating a transport or store does not write any data. When a store saves an object, the local transport writes a file atomically and restricts access to its owner.

  • .outpost/storage/
    • objects/One .object file per key, grouped by prefix.
      • checkpoints/Workflow runs, one per runId.
      • artifacts/Artifact bytes, addressed by digest.
      • task-cache/Cached task results.
      • logs/Journals.
      • resources/Activity of open sandboxes.
      • reservations/The storage reservation ledger.
      • speculations/Durable speculation state.
      • conversations/Archived conversations.
      • recovery/Archived recovery transfers.
    • .outpost/locks/Locks that serialize writers on this machine.

The rest of the .outpost directory is described in How it works.

Pass the same transport to the stores and to the dispatch options, and every object of a run lands in one place.

import {
  createLocalTransport,
  createWorkflowCheckpointStore,
  dispatch,
} from "@elie-laloum/outpost";
import { coder, repository, sandboxProvider } from "./outpost.config.ts";

const transporter = createLocalTransport({ directory: "/srv/outpost" });
const checkpoints = createWorkflowCheckpointStore({ transporter });

await dispatch({
  agent: coder,
  sandboxProvider,
  repository,
  brief: { text: "Update the changelog for the last release." },
  logging: { transporter },
  activityTransport: transporter,
  recoveryTransport: transporter,
});

Use a separate directory or prefix per project, so retention and access rules apply to one set of objects.

A remote transport keeps the same store contracts. S3 and R2 covers the setup; only the transporter line changes.

import { S3Client } from "@aws-sdk/client-s3";
import { createWorkflowCheckpointStore } from "@elie-laloum/outpost";
import { createS3Transport } from "@elie-laloum/outpost/transports/s3";

const client = new S3Client({ region: "eu-west-1" });
const transporter = createS3Transport({
  client,
  bucket: "my-private-outpost",
  prefix: "outpost/",
});
const checkpoints = createWorkflowCheckpointStore({ transporter });

// ... run your workflows, then:
client.destroy();

Your application owns the client. Closing a sandbox or finishing a workflow never closes it: destroy it once every operation using it has finished.

Every write names the revision it expects: ifRevision: null creates, the observed revision replaces or removes. If another writer changed the object first, the call throws TransportConflict and nothing is written.

import { reportValue } from "./reporter.ts";
import { createLocalTransport, TransportConflict } from "@elie-laloum/outpost";

const transporter = createLocalTransport({ directory: ".outpost/storage" });
const bytes = (text: string) => new TextEncoder().encode(text);

const first = await transporter.write("notes/today", bytes("v1"), {
  ifRevision: null,
});
await transporter.write("notes/today", bytes("v2"), {
  ifRevision: first.revision,
});
try {
  await transporter.write("notes/today", bytes("v3"), {
    ifRevision: first.revision,
  });
} catch (error) {
  if (error instanceof TransportConflict) reportValue("stale:", error.key);
  // Example output: stale: notes/today
}

It prints stale: notes/today. Stores use the same fence: a workflow that lost ownership of its checkpoint fails on its next write instead of overwriting a newer run. Re-read the object before you decide what to do.

  • The local transport coordinates processes on one machine; it does not provide distributed ownership over NFS or other shared mounts.
  • Listing returns current objects one by one, not a consistent snapshot of the prefix.
  • Revisions fence stale writers; they do not authenticate who wrote an object.
  • Keys are /-separated segments of letters, digits, ., _ and -, not starting with a dot, up to 512 characters.

API: Transport · createLocalTransport · TransportConflict · createWorkflowCheckpointStore · createArtifactStore · createTaskCacheStore · createTransportConversations · SandboxOptions · createS3Transport.