Skip to content
Helix Docs

Observability

Write structured logs with ctx.log, then read one traced entry per execution in the Helix dashboard. Local runs pretty-print to your terminal.

For developers Updated Oct 23, 2026
View as Markdown

Every function execution is observable without extra setup. Use ctx.log in your code for structured records; open an execution in the Helix dashboard, or with helix logs get, to see the full trace of that request.

What you get today:

  • Execution logs in the dashboard at https://app.helix.tray.ai. One entry per execution, HTTP request or scheduled job run; opening an entry shows the operations it ran, the third-party calls it made, and the request and response payloads.
  • Execution logs in the terminal with helix logs (CLI v1.7.0 and later). helix logs lists the same executions the dashboard shows, with flags to search them, keep only failures, or narrow the time window; helix logs get <executionId> prints one execution with its operations and ctx.log lines. See Logs commands.
  • ctx.log in your code, exactly as shown below. It is the supported way to add your own structured records to an execution.
  • Local logs in your terminal. helix dev --pretty-logs is the default and prints readable lines; helix dev --raw-logs prints the raw structured JSON instead.

Write logs with ctx.log

ctx.log is the primary logging interface. It produces structured, execution-scoped log records that appear on the execution’s timeline in the dashboard.

interface ContextLogger {
  info(message: string, metadata?: Record<string, unknown>): void;
  warn(message: string, metadata?: Record<string, unknown>): void;
  error(message: string, metadata?: Record<string, unknown>): void;
  debug(message: string, metadata?: Record<string, unknown>): void;
}
// functions/api/v1/sync.post.ts
import { defineFunction } from '@trayai/helix-sdk';
import { z } from 'zod';

export const input = z.object({
  userId: z.string(),
});

export default defineFunction<typeof input>(async (ctx) => {
  ctx.log.info('Starting sync', { userId: ctx.input.userId });

  const res = await ctx.http
    .authed('salesforce_prod')
    .get('https://acme.my.salesforce.com/services/data/v59.0/query', {
      query: { q: 'SELECT Id, Name FROM Contact LIMIT 10' },
    });
  const contacts = res.body as { records: Array<{ Id: string; Name: string }> };

  ctx.log.info('Fetched contacts', { count: contacts.records.length });

  await ctx.kv().set(`sync:last:${ctx.input.userId}`, {
    count: contacts.records.length,
    at: new Date().toISOString(),
  });

  ctx.log.warn('Duplicate check skipped (feature flag disabled)');
  return { synced: contacts.records.length };
});

Each call records:

  • Timestamp
  • Severity from the level (info, warn, error, debug)
  • Body, the message string
  • Attributes, your metadata object
  • Correlation with the current execution, so the record lands on the right timeline

In local dev, ctx.log also pretty-prints to the terminal for immediate visibility.

The dashboard shows spans and correlated log records on one time-ordered execution timeline:

exec_789xyz | 2025-03-19T10:30:00.123Z | POST /api/v1/sync

10:30:00.123  LOG  INFO   Starting sync { userId: "user_abc" }
10:30:00.145  SPAN HTTP → GET https://acme.my.salesforce.com/services/data/v59.0/query
                          auth: salesforce_prod
10:30:00.412  SPAN HTTP ← 200 OK (267ms)
10:30:00.413  LOG  INFO   Fetched contacts { count: 10 }
10:30:00.415  SPAN KV   → SET sync:last:user_abc
10:30:00.418  SPAN KV   ← OK (3ms)
10:30:00.450  LOG  WARN   Duplicate check skipped (feature flag disabled)
10:30:00.451  ←    200 OK { synced: 10 } (328ms total)

Full API reference: ctx.log.

Auto-instrumented operations

HTTP calls, KV operations, AI model calls, and agent runs appear as steps on the execution timeline without extra instrumentation.

HTTP calls. Authenticated calls via ctx.http.authed(...) capture the request method, URL, headers (credentials redacted), and body; the response status, headers, and body (truncated if large); the duration; and the auth alias used, plus whether a token refresh occurred. Unauthenticated calls capture method, URL, headers, response status, and duration.

KV operations. Every ctx.kv() operation produces a step with the operation type (get, set, delete, incr, list), the key (or prefix for list), and the duration.

AI model calls. Every ctx.ai call produces a step with the model, the alias when you called through one, and the input and output token counts the provider reported.

Agent runs. Every ctx.agent() run produces a step with the agent, the model, the session ID, the number of steps the run took, and how it ended: completed, awaiting approval, or error. Each tool the agent called appears as its own step, naming the agent and the tool.

Dev and production

You write the same code in both environments.

  • Local dev (helix dev). ctx.log pretty-prints to the terminal: colored level, readable timestamp, message, and metadata inline. Use --raw-logs for the structured JSON form.
  • Production. The same ctx.log calls appear on the execution timeline in the Helix dashboard and in helix logs get. Open an execution to see logs interleaved with the operations that ran. Nothing a deployed function logs reaches the helix dev terminal, and nothing helix dev prints reaches the platform.

console.log capture

console.log, console.warn, console.error, and console.debug are captured best-effort alongside ctx.log, so functions that haven’t adopted ctx.log still get some observability without refactoring. Prefer ctx.log for anything you need to rely on:

  • No structured attributes. console.log('foo', { bar: 1 }) produces a string; ctx.log.info('foo', { bar: 1 }) produces a log record with queryable attributes.
  • Third-party library output. Captured with the execution, but may lack useful structure.
  • Serialization. Complex objects may not serialize cleanly; ctx.log serializes reliably.
ScenarioUse
New code, structured loggingctx.log.info('message', { key: value })
Quick debugging during developmentconsole.log(...), goes to the terminal plus best-effort capture
Existing function that hasn’t adopted ctx.logconsole.log(...), captured best-effort
Production-critical loggingctx.log, always reliable and structured

Error tracking

Unhandled errors are captured automatically on the same execution timeline. The record includes the stack trace with source-mapped file paths, and the timeline shows everything up to the failure plus the request that triggered it:

exec_abc123 | 2025-03-19T10:30:00.123Z | GET /api/v1/users/:userId (ERROR)

10:30:00.123  LOG  INFO   Fetching user
10:30:00.200  SPAN HTTP → GET https://acme.my.salesforce.com/...
10:30:00.450  SPAN HTTP ← 401 Unauthorized (250ms)
10:30:00.451  LOG  ERROR  TypeError: Cannot read property 'records' of undefined
                          at handler (functions/api/v1/users/[userId].get.ts:15:34)
10:30:00.452  ←    500 Internal Server Error (329ms total)

Read logs

Open the project’s Logs tab in the Helix dashboard. Each entry is one execution, an HTTP request or a scheduled job run; open it for the full execution timeline. See the dashboard for the walkthrough.

From the terminal, helix logs lists the same executions newest first, and helix logs get opens one:

helix logs --errors --since 1h            # failed executions in the last hour
helix logs --search "POST /api/v1/sync"   # every call to one route in the last seven days
helix logs get 0a8c1f6e-9b2d-4c3a-8e1f-1a2b3c4d5e6f

helix logs get prints the execution’s status, trigger, duration, and actor, then the operations it ran with your ctx.log lines in place and any error with a short stack preview. Both commands take --json for scripts, and both need a deployed project. A coding agent reaches the same data through the CLI’s MCP server. The flags are in the Logs commands reference.

Loading search…

Jump to a section

tab to move · esc to close