Skip to content

Agent Guide

Do not guess which handlers are dark. Read them:

Terminal window
npx autotel map --json --no-write # every entry point
npx autotel map --json --no-write <route|file> # one entry point

Each entry in map.routes[].checks carries a status, a message, evidence (file, line, snippet), and a fix. Work from those. The checks land on the table below: trace wants a span, context wants a request logger with attributes, structured-errors wants createStructuredError with why and fix, error-handling wants catch blocks that record, audit wants withAudit() on money and auth paths.

autotel.map.json gets committed, so the score is trackable. A check that should not apply gets waived in code with // autotel-map-disable <check> -- reason rather than by lowering the CI threshold. See Observability Map.


Scenario Use Example
Wrap an async function with a span trace(fn) or span('Name', fn) Handlers, use-case functions, workers
Wrap with explicit name/key trace('checkout', fn) or instrument({ key: 'checkout', fn }) When name inference isn’t reliable
Run named work now with context trace.run('checkout', async (ctx) => { ctx.setAttribute(...); ... }) One-off operations, run right here
Reusable handler with context withTracing({})((ctx) => async (args) => { ctx.setAttribute(...); ... }) Wrapped functions that need context
One snapshot per request getRequestLogger(ctx?) + .set() / .info() / .error() + .emitNow() HTTP request handlers, background jobs
Throw an error with why/fix/link createStructuredError({ message, why?, fix?, link?, status?, cause? }) API routes, services, validation
Same error thrown from many places defineErrorCatalog('billing', { PAYMENT_DECLINED: { … } }) Repeated failures, .match() in catches
Set snapshot severity explicitly log.setLevel('warn') Degraded response with nothing to log
Show API error in UI (client) parseError(caught) → use message, why, fix, link Toasts, error banners, forms
Product/analytics events track('event.name', attributes) or Event from autotel/event Clicks, signups, conversions
Record error on current span recordStructuredError(ctx, error) or request logger .error() Inside catch blocks when you have a span
Security decision point securityEvent() from autotel-audit Login failure, access.tenant.violation
Wrap a sensitive operation withSecurity() from autotel-audit API key creation
Correlate actor without raw PII hashIdentifier() from autotel-audit Email or IP in actorId
Validation mismatch observability defineValidator() from autotel/validate POST body shape at boundary
Zero-code probe/401/LLM signals createSecuritySignalProcessor() in init({ spanProcessors }) Scanner traffic, credential stuffing
Join PostHog session to traces joinPostHog(posthog) from autotel-posthog in spanEnrichers Browser + PostHog on the same page

Rule of thumb: If there’s an HTTP request or a “job”, create a span (via trace() or framework middleware) and use getRequestLogger() when you want one coherent snapshot. Use createStructuredError for any error that should be explainable to users or agents.


1. Uninstrumented handler → trace + request logger

Section titled “1. Uninstrumented handler → trace + request logger”

Before:

export async function postCheckout(req: Request, res: Response) {
const user = await getAuth(req);
const body = await readBody(req);
const result = await processCheckout(user.id, body);
return res.json(result);
}

After:

import { trace, withTracing, getRequestLogger } from 'autotel';
export const postCheckout = withTracing({})(
(ctx) => async (req: Request, res: Response) => {
const log = getRequestLogger(ctx);
const user = await getAuth(req);
log.set({ user: { id: user.id } });
const body = await readBody(req);
log.set({ cart: { items: body.items?.length } });
const result = await processCheckout(user.id, body);
log.set({ result: { orderId: result.id } });
log.emitNow();
return res.json(result);
},
);

If the framework already creates a span per request (e.g. Autotel Hono middleware), you can call getRequestLogger() with no args inside the handler instead of passing ctx.

Before:

if (!user) throw new Error('User not found');
// or
catch (e) {
throw new Error('Payment failed');
}

After:

import { createStructuredError } from 'autotel';
if (!user) {
throw createStructuredError({
message: 'User not found',
status: 404,
why: `No user with ID "${userId}"`,
fix: 'Check the user ID and try again',
link: 'https://docs.example.com/errors/user-not-found',
});
}
try {
await processPayment(data);
} catch (e) {
throw createStructuredError({
message: 'Payment failed',
status: 402,
why: e instanceof Error ? e.message : 'Unknown error',
fix: 'Try a different payment method or contact support',
link: 'https://docs.example.com/payments',
cause: e,
});
}

3. Client: raw catch → parseError and UI

Section titled “3. Client: raw catch → parseError and UI”

Before:

try {
await fetch('/api/checkout', { method: 'POST', body: JSON.stringify(data) });
} catch (err) {
toast.error('Something went wrong');
}

After:

import { parseError } from 'autotel';
try {
await fetch('/api/checkout', { method: 'POST', body: JSON.stringify(data) });
} catch (err) {
const error = parseError(err);
toast.error(error.message, {
description: error.why,
action: error.fix
? { label: 'Fix', onClick: () => showHelp(error.fix) }
: undefined,
});
if (error.link) setDocLink(error.link);
}

4. Scattered console.log → request logger

Section titled “4. Scattered console.log → request logger”

Before:

export default defineEventHandler(async (event) => {
console.log('Checkout started');
const user = await requireAuth(event);
console.log('User:', user.id);
const cart = await getCart(user.id);
console.log('Cart items:', cart.items.length);
const result = await processCheckout(cart);
console.log('Order:', result.id);
return result;
});

After:

import { trace, withTracing, getRequestLogger } from 'autotel';
export default withTracing({})((ctx) => async (event) => {
const log = getRequestLogger(ctx);
const user = await requireAuth(event);
log.set({ user: { id: user.id } });
const cart = await getCart(user.id);
log.set({ cart: { items: cart.items.length } });
const result = await processCheckout(cart);
log.set({ order: { id: result.id } });
log.emitNow();
return result;
});

(If the framework attaches the event to an existing span, use getRequestLogger() with no args and omit the outer trace if the framework already creates the span.)

5. Failed login without telemetry → securityEvent() hook

Section titled “5. Failed login without telemetry → securityEvent() hook”

Before:

export const postLogin = withTracing({})((ctx) => async (req, res) => {
const { email, password } = req.body;
const user = await findUser(email);
if (!user || !(await verifyPassword(user, password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
return res.json({ token: await issueToken(user) });
});

After:

import { trace, withTracing } from 'autotel';
import { securityEvent, hashIdentifier } from 'autotel-audit';
export const postLogin = withTracing({})((ctx) => async (req, res) => {
const { email, password } = req.body;
const user = await findUser(email);
if (!user || !(await verifyPassword(user, password))) {
securityEvent({
name: 'auth.login.failed',
category: 'authentication',
outcome: 'failure',
severity: 'warning',
actorId: hashIdentifier(email),
reason: 'invalid_password',
});
return res.status(401).json({ error: 'Invalid credentials' });
}
return res.json({ token: await issueToken(user) });
});

The handler still enforces auth. Autotel records the signal. See Security Observability for setup and metrics.


import { Hono } from 'hono';
import { init, getRequestLogger } from 'autotel';
import { autotelMiddleware } from 'autotel-hono';
init({ service: 'my-api' });
const app = new Hono();
app.use('*', autotelMiddleware());
app.post('/api/checkout', async (c) => {
const log = getRequestLogger();
log.set({ route: 'checkout' });
log.emitNow();
return c.json({ ok: true });
});
import Fastify from 'fastify';
import { init, trace, withTracing, getRequestLogger } from 'autotel';
init({ service: 'my-api' });
// Register middleware that creates a span per request (see example app).
// In route handler:
app.post('/api/checkout', async (request, reply) => {
return withTracing({})((ctx) => async () => {
const log = getRequestLogger(ctx);
log.set({ route: 'checkout' });
const result = await handleCheckout(request);
log.emitNow();
return result;
})();
});

See packages/autotel-tanstack and apps/example-tanstack-start: middleware and env config. Use getRequestLogger() inside server handlers when a span is active.

Use autotel-cloudflare. It wraps fetch so each request gets a span and provides full bindings instrumentation (KV, R2, D1, Durable Objects, Workers AI, Vectorize, Queues, etc.). Use getRequestLogger() or trace context inside the handler.

Use the optional autotel-audit package for compliance and security telemetry:

  • Audit trail (withAudit, forceKeepAuditEvent, setAuditAttributes): who did what to which resource.
  • Security signals (securityEvent, withSecurity): abuse and detection events at auth, access control, and secret-handling decision points.
  • Zero-code HTTP signals: register createSecuritySignalProcessor() in init({ spanProcessors }) for probe paths, denied responses, and auth bursts.

The autotel-genai/agent entry point adds agent-layer audit (withAgentAction, withAgentToolCall, recordPolicyDecision) for tool calls, policy decisions, and delegation.

See Audit Logging and Security Observability.

Audit and agent telemetry is observability. A missing trace context must never crash the business logic it wraps. withAudit, withAgentAction, withAgentToolCall, recordPolicyDecision, and securityEvent / withSecurity no longer throw into your handler when there is no active trace context. An onMissingContext option (on each wrapper’s options arg) controls the behaviour:

  • 'warn' (default): run the wrapped handler un-audited and log one warning per action.
  • 'skip': run un-audited, silently.
  • 'throw': previous fail-fast behaviour: opt in when telemetry is mandatory.

This makes the agent layer safe to drop into a production hot path with no surrounding trace() and no try/catch.

The agent wrappers resolve trace context from any active OpenTelemetry span, not only inside autotel’s own trace(). They compose inside @effect/opentelemetry, a vanilla OpenTelemetry NodeSDK, and autotel-cloudflare-instrumented fetch handlers and Cloudflare Workflows. For example, inside a step.do(...) of an instrumentWorkflow-wrapped WorkflowEntrypoint:

import { withAgentToolCall } from 'autotel-genai/agent';
await step.do('research', async () =>
withAgentToolCall(
{
action: 'agent.research',
agent: { id: 'researcher' },
tool: { name: model, input: { prompt } },
},
() => env.AI.run(model, { messages }),
),
);
import { init, trace, withTracing, getRequestLogger } from 'autotel';
init({ service: 'my-api' });
server.on('request', (req, res) => {
withTracing({})((ctx) => async () => {
const log = getRequestLogger(ctx);
log.set({ method: req.method, path: req.url });
try {
const result = await handleRequest(req, res);
log.emitNow();
return result;
} catch (e) {
log.error(e);
log.emitNow();
throw e;
}
})();
});

Adding a New Framework Integration (touchpoints)

Section titled “Adding a New Framework Integration (touchpoints)”

When adding Autotel support for a new framework (e.g. a new web framework):

  1. New package or entry in existing package Create middleware/plugin that: (a) creates a span per request, (b) optionally runs in AsyncLocalStorage so getRequestLogger() can be called with no args.

  2. Touchpoints to update

    • New source: e.g. packages/autotel-<name>/src/index.ts (or new package).
    • Build: add entry in tsdown.config.ts / package build.
    • Exports: add in package.json exports and typesVersions.
    • Tests: add *.test.ts for middleware (span created, request logger available).
    • Example app: add under apps/example-<name> and wire init + middleware.
    • Docs: update AGENTS.md framework table and this guide with a short snippet.
    • Root: add workspace package and any scripts (e.g. pnpm --filter example-<name> start).
  3. Shared behavior Reuse existing patterns: one span per request, safe headers (no secrets), and optional integration with getRequestLogger() and createStructuredError in route handlers.

  4. Do not Use await import() for init; keep init synchronous. Do not add barrel re-exports that break tree-shaking.


  • Handlers wrapped with trace() or framework middleware that creates a span
  • Request-scoped context via getRequestLogger() and .emitNow() where needed
  • Thrown errors use createStructuredError({ message, why?, fix?, link?, status?, cause? })
  • Client uses parseError(err) and shows message/why/fix
  • No raw console.log for request/context when request logger is available
  • No await import() at init; use node-require helpers if needed
  • No secrets or full PII in attributes or logs
  • Security decision points emit securityEvent() or use withSecurity()
  • createSecuritySignalProcessor() registered in init() for zero-code HTTP signals
  • PII in security events uses hashIdentifier(), never raw values

Autotel ships a Claude Code skill at .claude/skills/autotel/ with detailed reference guides for wide events, structured errors, request loggers, and code review anti-patterns. See the Claude Code Skill page for full details.