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.
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.
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 contacts = await ctx.http
.authed('salesforce_prod')
.get('https://acme.my.salesforce.com/services/data/v59.0/query', {
searchParams: { q: 'SELECT Id, Name FROM Contact LIMIT 10' },
})
.json<{ 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.