ctx.log is the structured logger. Each call emits one log record correlated with the current execution, so log lines appear inline with HTTP and KV steps on the execution timeline. Use it instead of console.log, which is captured only best-effort.
ContextLogger
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;
}
| Parameter | Type | Description |
|---|---|---|
message | string | The log record body |
metadata | Record<string, unknown> (optional) | Structured context, serialized reliably |
All four methods are synchronous and return void; the level sets the record’s severity.
How metadata is carried
Metadata is kept as a structured object on the log record rather than flattened into one field per key. That preserves the full object for display and search while keeping cardinality bounded.
Correlation with the execution
Every record is tied to the current execution so it lands on the right timeline in the dashboard, alongside auto-instrumented HTTP and KV steps. Filter and search from the project’s Logs tab, or from the terminal with helix logs; helix logs get <executionId> prints each record as a log operation with its level, message, and metadata. See Logs commands.
Dev versus production
The code is identical in both environments. Under helix dev, ctx.log pretty-prints to the terminal (colored level, timestamp, message, and metadata inline). In production, the same records appear on the execution timeline in the Helix dashboard and in helix logs get.
Example
// functions/api/v1/sync.post.ts
import { z } from 'zod';
import { defineFunction } from '@trayai/helix-sdk';
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: unknown[] };
ctx.log.info('Fetched contacts', { count: contacts.records.length });
if (contacts.records.length === 0) {
ctx.log.warn('Nothing to sync');
}
return { synced: contacts.records.length };
});
The execution timeline, auto-instrumented steps, and console.log capture limits are covered in the observability guide.