Skip to main content

Logging and Sinks in TypeScript SDK

Sample available

A complete sample for setting up the instrumentation for the different components of the SDK is available on our samples repo.

Logging from Activities

Activities run in the standard Node.js environment and can use any Node.js logger.

Inject Activity context via interceptor and log all Activity executions


import { Context } from '@temporalio/activity';
import { ActivityInboundCallsInterceptor, ActivityExecuteInput, Next } from '@temporalio/worker';
import { Logger } from 'winston';

/** An Activity Context with an attached logger */
export interface ContextWithLogger extends Context {
logger: Logger;

/** Get the current Activity context with an attached logger */
export function getContext(): ContextWithLogger {
return Context.current() as ContextWithLogger;

/** Logs Activity executions and their duration */
export class ActivityInboundLogInterceptor implements ActivityInboundCallsInterceptor {
public readonly logger: Logger;

constructor(ctx: Context, logger: Logger) {
this.logger = logger.child({

// Set a logger instance on the current Activity Context to provide
// contextual logging information to each log entry generated by the Activity.
(ctx as ContextWithLogger).logger = this.logger;

async execute(input: ActivityExecuteInput, next: Next<ActivityInboundCallsInterceptor, 'execute'>): Promise<unknown> {
let error: any = undefined;
const startTime = process.hrtime.bigint();
try {
return await next(input);
} catch (err: any) {
error = err;
throw err;
} finally {
const durationNanos = process.hrtime.bigint() - startTime;
const durationMs = Number(durationNanos / 1_000_000n);
if (error) {
this.logger.error('activity failed', { error, durationMs });
} else {
this.logger.debug('activity completed', { durationMs });
Use the injected logger from an Activity


import { getContext } from './interceptors';

export async function greet(name: string): Promise<string> {
const { logger } = getContext();'Log from activity', { name });
return `Hello, ${name}!`;

Logging from Workflows with Workflow Sinks

Logging from Workflows is tricky for two reasons:

  1. Workflows run in a sandboxed environment and cannot do any I/O.
  2. Workflow code might get replayed at any time, generating duplicate log messages.

To work around these limitations, we recommend using the Sinks feature in the TypeScript SDK. Sinks enable one-way export of logs, metrics, and traces from the Workflow isolate to the Node.js environment.

Sinks are written as objects with methods. Similar to Activities, they are declared in the Worker and then proxied in Workflow code, and it helps to share types between both.

Comparing Sinks, Activities and Interceptors

Sinks are similar to Activities in that they are both registered on the Worker and proxied into the Workflow. However, they differ from Activities in important ways:

  • Sink functions don't return any value back to the Workflow and cannot not be awaited.
  • Sink calls are not recorded in Workflow histories (no timeouts or retries).
  • Sink functions are always run on the same Worker that runs the Workflow they are called from.

Declaring the Sink Interface

Explicitly declaring a Sink's interface is optional, but is useful for ensuring type safety in subsequent steps:


import { proxySinks, LoggerSinks, Sinks } from '@temporalio/workflow';

export interface AlertSinks extends Sinks {
alerter: {
alert(message: string): void;

export type MySinks = AlertSinks & LoggerSinks;

Implementing Sinks

Implementing Sinks is a two-step process.

Implement and inject the Sink function into a Worker


import { defaultSinks, InjectedSinks, Worker } from '@temporalio/worker';
import { MySinks } from './workflows';

async function main() {
const sinks: InjectedSinks<MySinks> = {
alerter: {
alert: {
fn(workflowInfo, message) {
console.log(`sending SMS alert!
workflow: ${workflowInfo.runId}
message: ${message}`);
callDuringReplay: false, // The default
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
taskQueue: 'sinks',
console.log('Worker gracefully shutdown');

main().catch((err) => {
  • Sink function implementations are passed as an object into WorkerOptions
  • You can specify whether you want the injected function to be called during Workflow replay by setting the callDuringReplay boolean option.

Proxy and call a Sink function from a Workflow


const { alerter, defaultWorkerLogger } = proxySinks<MySinks>();

export async function sinkWorkflow(): Promise<string> {'default logger: Workflow Execution started', {});
alerter.alert('alerter: Workflow Execution started');
return 'Hello, Temporal!';

Some important features of the InjectedSinkFunction interface:

  • Injected WorkflowInfo argument: The first argument of a Sink function implementation is a workflowInfo object that contains useful metadata.
  • Limited arguments types: The remaining Sink function arguments are copied between the sandbox and the Node.js environment using the structured clone algorithm.
  • No return value: To prevent breaking determinism, Sink functions cannot return values to the Workflow.

Advanced: Performance considerations and non-blocking Sinks

The injected sink function contributes to the overall Workflow Task processing duration.

  • If you have a long-running sink function, such as one that tries to communicate with external services, you might start seeing Workflow Task timeouts.
  • The effect is multiplied when using callDuringReplay: true and replaying long Workflow histories because the Workflow Task timer starts when the first history page is delivered to the Worker.

Logging in Workers and Clients

The Worker comes with a default logger which defaults to log any messages with level INFO and higher to STDERR using console.error. There are 5 levels in total: TRACE, DEBUG, INFO, WARN, and ERROR.

The reason we only offer a default logger is to minimize Worker dependencies and allow SDK users to bring their own logger.

Customizing the default logger

Temporal ships a DefaultLogger that implements the basic interface:

Example: Set up the DefaultLogger to only log messages with level WARN and higher

import { DefaultLogger, Runtime } from '@temporalio/worker';

const logger = new DefaultLogger('WARN', ({ level, message }) => {
console.log(`Custom logger: ${level}${message}`);
Runtime.install({ logger });

Example: Accumulate logs for testing/reporting

import { DefaultLogger, LogEntry } from '@temporalio/worker';

const logs: LogEntry[] = [];
const logger = new DefaultLogger('TRACE', (entry) => logs.push(entry));
log.debug('hey', { a: 1 });'ho');
log.warn('lets', { a: 1 });

The log levels are listed here in increasing order of severity.

Using a custom logger

A common logging use case is logging to a file to be picked up by a collector like the Datadog Agent.

import { Runtime } from '@temporalio/worker';
import winston from 'winston';

const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new transports.File({ filename: '/path/to/worker.log' })],
Runtime.install({ logger });


For information about metrics, see the Metrics section on the Deploy Checklist page.

OpenTelemetry tracing

The interceptors-opentelemetry sample shows how to use the SDK's built-in OpenTelemetry tracing to trace everything from starting a Workflow to Workflow Execution to running an Activity from that Workflow.

The built-in tracing uses protobuf message headers (like this one when starting a Workflow) to propagate the tracing information from the client to the Workflow and from the Workflow to its successors (when Continued As New), children, and Activities. All of these executions are linked with a single trace identifier and have the proper parent->child span relation.

Tracing is compatible between different Temporal SDKs as long as compatible context propagators are used.

Context propagation

The TypeScript SDK uses the global OpenTelemetry propagator.

To extend the default (Trace Context and Baggage propagators) to also include the Jaeger propagator, follow these steps:

  • npm i @opentelemetry/propagator-jaeger

  • At the top level of your Workflow code, add the following lines:

    import { propagation } from '@opentelemetry/api';
    import {
    } from '@opentelemetry/core';
    import { JaegerPropagator } from '@opentelemetry/propagator-jaeger';

    new CompositePropagator({
    propagators: [
    new W3CTraceContextPropagator(),
    new W3CBaggagePropagator(),
    new JaegerPropagator(),

Similarly, you can customize the OpenTelemetry NodeSDK propagators by following the instructions in the Initialize the SDK section of the README.