Skip to content
BLACKLAKE

Concepts

Workflows

A workflow is a named TypeScript async function that receives a context object (ctx) and returns a result. Workflows are defined with workflow() and exported as the default export of a .ts file.

export default workflow('my-workflow', async (ctx) => {
  // steps go here
  return result;
});

Each time a workflow runs, Depth creates a run record in SQLite. The run tracks status (running, completed, failed, paused), input, output, and total cost.

Steps

A step is the core primitive of Depth. Every unit of work that should be persisted — an LLM call, a file write, an API call — should be wrapped in a step.

const result = await step(ctx, 'step-name', async () => {
  // work here
  return value;
});

Steps are identified by (run_id, name). Names must be unique within a workflow run.

Replay

When a workflow runs, Depth loads all previously completed steps from the database. If a step with a matching name is found, its stored output is returned immediately — the function is not called again.

This means:

  • Killing and restarting a process picks up where it left off
  • LLM calls are not repeated for completed steps
  • Side effects in steps may re-execute if the step did not complete before the process exited

Step output

Step output must be JSON-serializable. Objects, arrays, strings, numbers, and booleans all work. Functions and class instances do not.

LLM Routing

ctx.llm(model, options) calls an LLM and returns the text response. The model parameter uses a provider:model format:

StringProviderNotes
anthropic:claude-sonnet-4-6AnthropicRequires ANTHROPIC_API_KEY
anthropic:claude-opus-4AnthropicRequires ANTHROPIC_API_KEY
openai:gpt-4oOpenAIRequires OPENAI_API_KEY
openai:gpt-4o-miniOpenAIRequires OPENAI_API_KEY
ollama:llama3OllamaLocal, no key needed

LLM calls record token counts and estimated cost in the step's database row.

Cost Tracking

Every LLM call records:

  • Input and output token counts
  • Cost in USD (from a built-in pricing table)

Costs roll up to the run's total_cost_usd field. View them with depth status <run_id> or depth list.

Tool Calls

ctx.tool(name, args) executes a named tool. When Surface is running, tool calls are routed through Surface's governance engine before execution.

await ctx.tool('filesystem.writeFile', {
  path: './output.md',
  content: 'Hello',
});

Surface Integration

BlackLake Surface is the governance and observability console for AI agents. When Surface is running at localhost:3100, Depth automatically:

  1. Governs tool calls — every ctx.tool() call is sent to Surface's /v1/govern endpoint. Surface evaluates it against your policies and returns a decision: allow, deny, or approval_required.

  2. Manages approvals — if a tool call requires approval, Depth pauses the run and waits. A reviewer approves or rejects in the Surface console, and the run resumes automatically.

  3. Tracks costs — LLM usage appears in Surface's usage dashboard.

No configuration is needed. Depth checks localhost:3100/health once at workflow start. If Surface is not running, tool calls execute directly without governance.

To use a different Surface URL:

export BLACKLAKE_SURFACE_URL=http://localhost:3100

Durable Approval Gates

ctx.waitForApproval(reason) pauses a workflow and waits for a human to approve before continuing.

await step(ctx, 'get-approval', async () => {
  await ctx.waitForApproval('Review the draft before sending');
  return 'approved';
});

The run's status is set to paused. The workflow unpauses when a signal named approval arrives in the depth_signals table.

To approve from the CLI:

depth signal <run_id> approval

If Surface is running, approvals are created in Surface's console and the signal is injected automatically when approved.

Signals

ctx.waitForSignal(name) pauses a workflow until an arbitrary named signal arrives.

const payload = await ctx.waitForSignal('data-ready');

Send a signal from the CLI:

depth signal <run_id> data-ready '{"rows": 42}'

Durable Timers

ctx.waitFor(durationMs) pauses a workflow for a duration. If the process restarts after the duration has elapsed, the wait resolves immediately.

await ctx.waitFor(60_000); // wait 1 minute

Database

Depth stores all state in ~/.blacklake/blacklake.db — the same SQLite database used by BlackLake Surface. Depth uses a depth_ table prefix to avoid collisions.

The database path can be overridden:

export BLACKLAKE_DB_PATH=/path/to/custom.db