Logging & observability
Your tools and workflow steps log with plain
console.*. The platform stamps each line with the run (and session) it belongs to, stores it, and renders the run timeline in Studio. You never wire a tracer or a log exporter yourself.
A Stackbone workspace is made of two durable units: deep agents, conversations that hold state across turns, and durable workflows, multi-step processes that checkpoint after every step. Both run on infrastructure that records what happened, so observability is something you read, not something you instrument.
Logging from a tool or a workflow step
You write code in two places: a deep-agent tool, and the steps
of a durable workflow. In both, reach for console.*: every line is
captured and attributed to the current run (and, for an agent, the
session) automatically. You never thread a run id.
A tool, defined inline in deep-agents/<name>/index.ts:
import { tool } from '@langchain/core/tools';
import { stackbone, z } from '@stackbone/sdk';
const lookupOrder = tool(
async ({ orderId }: { orderId: string }) => {
console.info('looking up order', { orderId });
const rows = await stackbone.database.select().from(orders).where(eq(orders.id, orderId));
if (rows.length === 0) {
console.warn('order not found', { orderId });
}
return rows[0] ?? null;
},
{
name: 'lookup_order',
description: 'Look up an order by id.',
schema: z.object({ orderId: z.string() }),
},
);A durable workflow step in workflows/<name>.workflow.ts:
async function notifyPartner(partner: string) {
'use step';
console.warn('partner rate limited, backing off', { partner });
// ... do work ...
}tool(...) comes from the upstream
LangChain package; 'use step'
is a Workflow
SDK directive.
The stackbone handle is the ambient client, the
same object on every surface, no construction required.
Pass fields as an object, not in the message
console.* lines are captured with structured fields when you pass an
object alongside the message. Studio can filter and search on those
fields, so this:
console.info('email sent', { to, templateId });is more useful later than interpolating the same value into the string:
console.info('email sent to ' + to); // no structured `to` to filter onKeep the message a short, stable phrase and put the variable data in the object.
The run timeline is automatic
You do not instrument anything to get the timeline. Open a run in Studio and you see a waterfall built from what the runtime already records:
- Durable workflow steps. Each
'use step'function is a checkpoint. It appears on the run with its timing, its result, and — if it failed and retried — each attempt. This is the core observability primitive of a workflow: the event-log replay model means the run is its list of steps. - Agent turns and tool calls. Every turn in a session, and every tool the model invoked during that turn, shows up with its inputs, outputs and timing.
- Model calls. Calls through
stackbone.ai(or a direct Vercel AI SDK call routed through the gateway) record latency, token usage and cost on the step.
So there is no span processor to register and no exporter to configure
in your code. You log with console.*; the timeline comes for free.
Runs that pause
A workflow that hits requestApproval() or a
durable hook stops and shows on the timeline as interrupted /
pending while it waits for a human. When the decision arrives the run
resumes from exactly that checkpoint and the rest of the steps appear.
Reading the timeline is how you see where a long-lived run is parked.
console.log is the way
If you reach for console.log out of habit, it does the right thing.
A console.* line emitted inside a deep-agent tool or inside a
workflow step is captured and tagged with the run and session it
belongs to. Outside a run (say in module top-level code that runs once
at boot) it just prints normally, untagged.
The only caveat is structured fields: they survive only if you pass an
object argument. console.error('refund failed', { code, orderId })
keeps code and orderId filterable; console.error('refund failed: ' + code) flattens everything into the message.
Inspecting runs from the shell
You do not need Studio open to read what happened. The CLI inspects the same runs and logs:
# List recent runs, newest first.
stackbone runs list
# Inspect one run: status, steps, timing, errors.
stackbone runs get <run-id>
# Live-tail logs across the installation.
stackbone logs tail --follow
# Tail only the lines for one run.
stackbone logs tail --run <run-id> --follow
# List the workflows in the workspace.
stackbone workflows liststackbone runs retry <run-id> and stackbone runs cancel <run-id>
act on a run once you have found it.
Local development
When you run your workspace with stackbone dev, the same logs and run
timeline are served locally — the durable runtime, the run records, and
the captured console.* lines all behave exactly as they do once
deployed. So what you see while iterating matches production behaviour;
you debug against the real model, not a mock of it.
Where to go next
stackbone.ai— the model calls that appear as steps on the timeline, with token usage and cost.- Agents & sessions — turns, runs and the session model your tool logs are attributed to.
- Workflows —
'use step'checkpoints, replay, and how a run is reconstructed from its steps. - Human approvals —
requestApproval()and how a paused run reads on the timeline. - CLI reference —
stackbone runs,stackbone logs tailandstackbone workflows.