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:
| String | Provider | Notes |
|---|---|---|
anthropic:claude-sonnet-4-6 | Anthropic | Requires ANTHROPIC_API_KEY |
anthropic:claude-opus-4 | Anthropic | Requires ANTHROPIC_API_KEY |
openai:gpt-4o | OpenAI | Requires OPENAI_API_KEY |
openai:gpt-4o-mini | OpenAI | Requires OPENAI_API_KEY |
ollama:llama3 | Ollama | Local, 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:
-
Governs tool calls — every
ctx.tool()call is sent to Surface's/v1/governendpoint. Surface evaluates it against your policies and returns a decision:allow,deny, orapproval_required. -
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.
-
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