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 on

Keep 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 list

stackbone 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 approvalsrequestApproval() and how a paused run reads on the timeline.
  • CLI referencestackbone runs, stackbone logs tail and stackbone workflows.

The platform for agent developers.Build, ship and observe agentsthat survive prod.

© 2026 · STACKBONEBUILT WITH ❤️ FROM CANADA AND SPAIN