Metadata reference TypeScript only

step.metadata() is a step tool, like step.run(), step.sleep(), or step.waitForEvent(). It attaches custom key-value data to your function runs. Like every step tool, it takes a memoization ID, executes exactly once, and appears as its own step in the trace.

The data you attach shows up in the Inngest dashboard trace view. Use it for tracking processing status, recording business-level metrics, or annotating runs with contextual information.

This page covers the API surface. For practical usage examples, see the metadata how-to guide.

Setup

Add metadataMiddleware() to your Inngest client:

import { Inngest } from "inngest";
import { metadataMiddleware } from "inngest/experimental";

const inngest = new Inngest({
  id: "my-app",
  middleware: [metadataMiddleware()],
});

This makes step.metadata() available inside function handlers and inngest.metadata available on the client instance.

step.metadata(memoId)

Creates a step that attaches metadata to the current run. Like step.run("my-id", ...) or step.sleep("my-id", ...), the memoId is a unique step ID that ensures the update runs exactly once, even if the function re-executes.

MethodSignatureDescription
.run(id?)(id?: string) => MetadataBuilderScope to a run. Omit id for the current run.
.step(id?)(id?: string) => MetadataBuilderScope to a step. Omit id for the current step. Defaults to the current or latest attempt.
.span(id)(id: string) => MetadataBuilderScope to an extended trace span.
.update(values, kind?)(values: Record<string, unknown>, kind?: string) => Promise<void>Send the metadata update. kind defaults to "default".
.do(fn)(fn: (builder: MetadataBuilder) => Promise<void>) => Promise<void>Batch multiple updates in a single memoized step.

inngest.metadata

The client-level metadata builder. Same scoping methods as step.metadata(), but without .do(). Can be called inside step.run(), in the function body, or from external contexts.

When called inside a step.run() callback, the metadata update is batched with the step's output. This makes it durable without creating an additional step.

MethodSignatureDescription
.run(id?)(id?: string) => MetadataBuilderScope to a run.
.step(id?)(id?: string) => MetadataBuilderScope to a step. Defaults to the current or latest attempt.
.span(id)(id: string) => MetadataBuilderScope to an extended trace span.
.update(values, kind?)(values: Record<string, unknown>, kind?: string) => Promise<void>Send the metadata update.

Scoping

Metadata attaches at different levels of granularity. Builder methods narrow the scope. They are chainable and each one removes itself from the available methods, preventing invalid combinations.

ScopeBuilder methodDescriptionDefault when...
run.run(id?)Attached to the entire function runOutside step.run()
step.step(id?)Attached to a specific step attemptInside step.run()
extended_trace.span(id)Attached to an extended trace spanNever (must be explicit)

When you call .update() without setting a scope, the builder auto-detects: inside step.run() it defaults to the current step's attempt; outside step.run() it defaults to run scope.

Metadata is always attached at the step attempt level. When a step retries, each attempt gets its own metadata. Previous attempts' metadata is preserved.

Custom kinds

The second argument to .update() specifies a metadata kind. Kinds are used to group related metadata together. When multiple updates share the same kind and scope, their values are merged.

// Default kind: stored as "userland.default", displayed as "User Metadata"
await step.metadata("id-1").update({ status: "processing" });

// Custom kind: stored as "userland.billing", displayed as "User Metadata (billing)"
await step.metadata("id-2").update(
  { invoiceId: "inv_123", total: 99.99 },
  "billing",
);

// Another custom kind: stored as "userland.analytics", displayed as "User Metadata (analytics)"
await step.metadata("id-3").update(
  { source: "organic", campaign: "spring-2025" },
  "analytics",
);
Kind stringStored asDashboard label
(omitted or "default")userland.defaultUser Metadata
"billing"userland.billingUser Metadata (billing)
"analytics"userland.analyticsUser Metadata (analytics)

The inngest.* namespace is reserved for system use. Custom kinds must not use this prefix.

Built-in metadata kinds

Inngest automatically attaches system metadata when applicable. These are read-only and managed by the platform. The inngest.* namespace is reserved for system use.

KindLabel in dashboardDescription
inngest.aiAI MetadataLLM/AI gateway data: input/output tokens, model, latency, cost
inngest.httpHTTP MetadataHTTP request details: method, status code, request/response sizes
inngest.http.timingHTTP TimingTiming breakdown: DNS, TCP, TLS, TTFB, transfer
inngest.response_headersResponse HeadersHTTP response header key-value pairs
inngest.warningsWarningsWarning messages from the execution engine

Size limits

Metadata is subject to the following size limits, enforced server-side:

LimitMaximum sizeDescription
Per update64 KBMaximum size of a single metadata update (span)
Per run1 MBCumulative size of all metadata across the entire function run

How size is calculated

Size is calculated as the sum of each key's byte length plus the JSON-serialized byte length of each value:

size = sum(len(key) + len(JSON.stringify(value))) for each entry

For example, { "status": "ok" } costs approximately 6 + 4 = 10 bytes (key "status" = 6 bytes, value "ok" serialized = 4 bytes including quotes).

Error handling

When a size limit is exceeded, the server returns an HTTP 413 error:

ErrorMessage
Per-update exceeded"Metadata span exceeds maximum size of 64KB"
Per-run exceeded"Cumulative metadata size exceeds limit"

If you're working with large metadata payloads, consider storing the data externally (e.g., in a database or object store) and attaching only a reference ID or URL as metadata.

Limitations

  • TypeScript SDK only. Python and Go SDK support is planned but not yet available.
  • Merge-only. The only supported operation is merge, which combines values of the same kind. You cannot delete or replace individual metadata keys.
  • Unique memoization IDs. Each step.metadata(memoId) creates a step in the run. The memoId must be unique within the function, just like any other step ID.
  • Server-side enforcement. Size limits are enforced by the server, not the SDK. Exceeding limits returns HTTP 413 errors that surface as exceptions in your function.