Logging

The Inngest SDK uses Pino-style object-first logging, where structured data is passed before the message string:

logger.info({ userId: "abc123" }, "Processing user event");

A logger is available on the function context as ctx.logger. It provides .info(), .warn(), .error(), and .debug() methods.

export default inngest.createFunction(
  {
    id: "process-upload",
    triggers: { event: "app/file.uploaded" },
  },
  async ({ event, step, logger }) => {
    logger.info({ fileId: event.data.fileId }, "Starting upload processing");

    const result = await step.run("process", () => {
      logger.debug({ fileId: event.data.fileId }, "Processing file");
      return processFile(event.data.fileId);
    });

    logger.info({ fileId: event.data.fileId, result }, "Upload processed");
    return result;
  }
);

Logger interface

Any object that implements these four methods can be used as a logger:

interface Logger {
  info(...args: any[]): void;
  warn(...args: any[]): void;
  error(...args: any[]): void;
  debug(...args: any[]): void;
}

Setting a logger

Pass a logger to the logger option on the Inngest client. This logger will be available on ctx.logger in all functions.

import pino from "pino";
import { Inngest } from "inngest";

const logger = pino({ level: "debug" });

export const inngest = new Inngest({
  id: "my-app",
  logger: logger,
});

Object-first vs string-first loggers

The SDK expects object-first loggers (like Pino), where structured data comes before the message:

// Object-first (Pino style) - works out of the box
logger.info({ userId: "abc" }, "User created");

Some loggers like Winston use string-first conventions, where the message comes first:

// String-first (Winston style) - needs wrapping
logger.info("User created", { userId: "abc" });

For string-first loggers, use wrapStringFirstLogger to adapt them:

import { wrapStringFirstLogger } from "inngest";
import winston from "winston";

const winstonLogger = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

const logger = wrapStringFirstLogger(winstonLogger);

Default logger

If no logger is set, the SDK uses a built-in ConsoleLogger that defaults to "info" level. You can customize the level:

import { ConsoleLogger, Inngest } from "inngest";

export const inngest = new Inngest({
  id: "my-app",
  logger: new ConsoleLogger({ level: "debug" }),
});

ConsoleLogger is intended for local development. For production, use a structured logger like Pino.

Internal logger

The SDK produces two categories of logs:

  • Function logs - your logs via ctx.logger inside Inngest functions
  • SDK internal logs - registration, request handling, middleware errors, etc.

By default, both use the same logger. Set internalLogger to route SDK internal logs separately:

import pino from "pino";
import { Inngest } from "inngest";

const logger = pino();

export const inngest = new Inngest({
  id: "my-app",
  logger: logger,
  internalLogger: logger.child({ component: "inngest-sdk" }),
});

This is useful for filtering or routing SDK internals to a different destination without affecting your function logs.

import pino from "pino";
import { Inngest } from "inngest";

const appLogger = pino({ level: "info" });
const sdkLogger = pino({ level: "warn" });

export const inngest = new Inngest({
  id: "my-app",
  logger: appLogger,
  internalLogger: sdkLogger,
});

If internalLogger is not set, it falls back to logger.