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 logslists 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 andctx.loglines. See Logs commands. ctx.login 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-logsis the default and prints readable lines;helix dev --raw-logsprints 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.logpretty-prints to the terminal: colored level, readable timestamp, message, and metadata inline. Use--raw-logsfor the structured JSON form. - Production. The same
ctx.logcalls appear on the execution timeline in the Helix dashboard and inhelix logs get. Open an execution to see logs interleaved with the operations that ran. Nothing a deployed function logs reaches thehelix devterminal, and nothinghelix devprints 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.logserializes reliably.
| Scenario | Use |
|---|---|
| New code, structured logging | ctx.log.info('message', { key: value }) |
| Quick debugging during development | console.log(...), goes to the terminal plus best-effort capture |
Existing function that hasn’t adopted ctx.log | console.log(...), captured best-effort |
| Production-critical logging | ctx.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.