Skip to content
Helix Docs

ctx.log

Reference for ctx.log with the info, warn, error, and debug signatures, metadata, and how logs appear locally versus in the dashboard.

For developers Updated Oct 23, 2026
View as Markdown

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;
}
ParameterTypeDescription
messagestringThe log record body
metadataRecord<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.

Loading search…

Jump to a section

tab to move · esc to close