Skip to content
Français

Connect tasks and dependencies

Define tasks, declare what they depend on and read their typed results.

Declare each step with a task constructor, then give the tasks to defineWorkflow(). Dependencies determine execution order and which earlier results a task may read.

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

const files = defineTask({ key: "files", perform: () => ["src/parser.ts"] });
const report = defineTask({
  key: "report",
  after: [files],
  perform: (context) => ({ reviewed: context.value(files).length }),
});
const result = await defineWorkflow("review", [files, report]).start();
result.unwrap();
reportValue(result.value(report));
// Example output: { reviewed: 1 }

It prints { reviewed: 1 }. defineTask() and defineWorkflow() only declare the graph: nothing runs until start().

List a task in after, then read its output with context.value(task). The value keeps the type returned by that task’s perform.

context.value() throws for a task missing from after, even if it already ran. A task starts only once every task in its after list is done.

defineWorkflow() checks the graph before anything runs and throws on the first error.

MistakeError
Two tasks share a keyDuplicate task: test
A task in after is not in the workflow’s listpublish: missing dependency lint
Tasks depend on each other in a loopDependency cycle at report
A key does not match [A-Za-z0-9][A-Za-z0-9._-]*Invalid task key: …, from defineTask()

start() resolves with a WorkflowResult once no task can run any more, even when tasks failed. It rejects when an option is invalid or a checkpoint cannot be saved.

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

const lint = defineTask({
  key: "lint",
  perform: () => {
    throw new Error("2 lint errors");
  },
});
const test = defineTask({ key: "test", perform: () => "ok" });
const result = await defineWorkflow("checks", [lint, test]).start();
reportValue(result.status);
// Example output: failed
for (const task of result.tasks)
  reportValue(task.key, task.status, task.error ?? "");
// Example output: lint failed 2 lint errors

It prints failed, then lint failed 2 lint errors and test cancelled: by default, the first failure cancels the tasks that have not finished.

API reference: WorkflowResult and TaskRecord.

start() runs one task at a time by default, in list order. Pass concurrency to run independent tasks together.

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

const lint = defineTask({ key: "lint", perform: () => ({ warnings: 0 }) });
const test = defineTask({ key: "test", perform: () => ({ failed: 0 }) });
const report = defineTask({
  key: "report",
  after: [lint, test],
  perform: (context) =>
    context.value(lint).warnings + context.value(test).failed === 0,
});
const result = await defineWorkflow("checks", [lint, test, report]).start({
  concurrency: 2,
});
result.unwrap();
reportValue(result.value(report));
// Example output: true

lint and test run together, then report prints true. Retries, timeouts and what a failure stops are on Concurrency, retries and timeouts.

condition runs before the task’s first attempt. When it returns false, the task ends as skipped without running.

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

const changes = defineTask({ key: "changes", perform: (): string[] => [] });
const review = defineTask({
  key: "review",
  after: [changes],
  condition: (context) => context.value(changes).length > 0,
  perform: (context) => `Reviewed ${context.value(changes).length} files`,
});
const result = await defineWorkflow("review", [changes, review]).start();
reportValue(
  result.status,
  result.tasks.map((task) => task.status),
);
// Example output: done [ 'done', 'skipped' ]

It prints done [ 'done', 'skipped' ]. A skipped task does not fail the run, has no value, and skips every task that depends on it.

diagram() returns the graph as a Mermaid flowchart, for a README or a pull request.

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

const lint = defineTask({ key: "lint", perform: () => 0 });
const report = defineTask({ key: "report", after: [lint], perform: () => 0 });
reportValue(defineWorkflow("checks", [lint, report]).diagram());
// Example output: flowchart LR
flowchart LR
  n0["lint"]
  n1["report"]
  n0 --> n1

defineAgentTask() and defineCommandTask() run in a sandbox you opened with createSandbox(). The tasks share its files; you close it.

import type { Sandbox } from "@elie-laloum/outpost";
import { defineAgentTask } from "@elie-laloum/outpost";

export function defineFix(sandbox: Sandbox) {
  return defineAgentTask({
    key: "fix",
    sandbox,
    request: () => ({ brief: { text: "Fix the failing date tests." } }),
  });
}
import type { Sandbox } from "@elie-laloum/outpost";
import { defineFix } from "./fix-dates.ts";
import { defineCommandTask } from "@elie-laloum/outpost";

export function defineTests(
  sandbox: Sandbox,
  fix: ReturnType<typeof defineFix>,
) {
  return defineCommandTask({
    key: "test",
    after: [fix],
    sandbox,
    command: { executable: "npm", arguments: ["test"] },
  });
}
import { createSandbox, defineWorkflow } from "@elie-laloum/outpost";
import { repository, sandboxProvider, coder } from "./outpost.config.ts";
import { defineFix } from "./fix-dates.ts";
import { defineTests } from "./test-dates.ts";

await using sandbox = await createSandbox({
  repository,
  sandboxProvider,
  agent: coder,
});
export const fix = defineFix(sandbox);
export const test = defineTests(sandbox, fix);
export const result = await defineWorkflow("fix-dates", [fix, test]).start();
result.unwrap();

test runs npm test on the agent’s edits. A nonzero exit status fails the task.

Each declaration returns a task that you list in defineWorkflow() and connect with after.

DeclarationUse it forGuide
defineTaskYour own code returning a value.This page
defineIsolatedTaskAn agent task in its own sandbox, opened and closed by the task.From a task to a workflow
defineAgentTaskAn agent turn in a sandbox you keep open.Share a sandbox
defineCommandTaskA command in a sandbox you keep open.Share a sandbox
defineLoopTaskAttempts checked in rounds, with the failed check as feedback.Verification loops
defineQueuedTaskWork handed to a worker through a job queue.Job queues and workers
defineApprovalTaskA pause until a listed person approves or rejects.Approvals
definePauseTaskA pause until a listed person resumes or rejects.Approvals
defineInteractiveAgentTaskAn agent dialogue that waits for human answers between turns.Interactive tasks
defineArtifactTaskA value published as an artifact; dependents receive a reference.Artifacts
defineWorkflowJobNot a task: runs a whole workflow as a queue job.Job queues and workers
  • Outputs live in memory for one start(). A restarted run reruns every task unless you pass a checkpoint.
  • Approval and pause tasks, interactive tasks, quota pauses, answers and decisions require a checkpoint: start() throws without one.
  • A workflow does not commit, merge or push across tasks as one transaction. To change several repositories, see Multiple repositories.

API: defineTask · defineWorkflow · TaskContext · WorkflowResult · TaskRecord · WorkflowFailure