You can attach up to 256 KB of metadata to a run. Metadata is accessible from inside the run function, via the REST API, Realtime , and in the dashboard. Use it to store additional structured information — for example, user identifiers, progress state, intermediate results, or deployment status.
Pass a metadata object when triggering a run:
import { myTask } from "./trigger/my-task" ;
const handle = await myTask . trigger (
{ message: "hello world" },
{ metadata: { user: { name: "Eric" , id: "user_1234" } } }
);
Use metadata.current() to get the entire metadata object, or metadata.get(key) to retrieve a specific key:
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
// Get the whole metadata object
const currentMetadata = metadata . current ();
console . log ( currentMetadata );
// Get a specific key
const user = metadata . get ( "user" );
console . log ( user . name ); // "Eric"
},
});
Both methods work anywhere inside the run function — including helper functions called from it:
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
doSomeWork ();
},
});
function doSomeWork () {
metadata . set ( "progress" , 0.5 ); // Works — called within the run context
}
Calling metadata.current() or metadata.get() outside of a run function always returns
undefined. Calling update methods outside of a run is a no-op. This makes it safe to use
metadata methods anywhere in shared utilities without guarding.
Metadata methods also work inside task lifecycle hooks:
myTasks.ts
trigger.config.ts
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
// run logic
},
onStart : async () => {
metadata . set ( "progress" , 0 );
},
onSuccess : async () => {
metadata . set ( "progress" , 1.0 );
},
});
Updates API
All update methods (except flush and stream) are synchronous and non-blocking. The SDK periodically flushes changes to the database in the background, so you can call them as often as needed without impacting run performance.
set
Set the value of a specific key:
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
metadata . set ( "progress" , 0.1 );
// ... do work ...
metadata . set ( "progress" , 0.5 );
// ... do more work ...
metadata . set ( "progress" , 1.0 );
},
});
del
Delete a key from the metadata object:
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
metadata . set ( "progress" , 0.5 );
// Remove the key entirely
metadata . del ( "progress" );
},
});
replace
Replace the entire metadata object:
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
metadata . set ( "progress" , 0.1 );
// Discard all existing metadata and start fresh
metadata . replace ({ user: { name: "Eric" , id: "user_1234" } });
},
});
append
Append a value to an array key (creates the array if the key does not exist):
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
metadata . append ( "logs" , "Step 1 complete" );
metadata . append ( "logs" , "Step 2 complete" );
console . log ( metadata . get ( "logs" )); // ["Step 1 complete", "Step 2 complete"]
},
});
remove
Remove a specific value from an array key:
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
metadata . append ( "logs" , "Step 1 complete" );
metadata . remove ( "logs" , "Step 1 complete" );
console . log ( metadata . get ( "logs" )); // []
},
});
increment / decrement
Atomically increment or decrement a numeric key:
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
metadata . set ( "progress" , 0.1 );
metadata . increment ( "progress" , 0.4 ); // now 0.5
metadata . decrement ( "progress" , 0.2 ); // now 0.3
},
});
flush
Force an immediate write to the database. Use this when you need the metadata to be persisted before a checkpoint or external call:
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
metadata . set ( "progress" , 0.5 );
// Ensure persisted before the next await
await metadata . flush ();
},
});
Fluent API
All update methods return the updater object, so you can chain them:
import { task , metadata } from "@trigger.dev/sdk" ;
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
metadata
. set ( "progress" , 0.1 )
. append ( "logs" , "Step 1 complete" )
. increment ( "progress" , 0.4 )
. set ( "status" , "running" );
},
});
Parent & root updates
A child task can update the metadata of its parent or the root task. This is useful for rolling up progress across a task hierarchy.
import { task , metadata } from "@trigger.dev/sdk" ;
export const myParentTask = task ({
id: "my-parent-task" ,
run : async ( payload : { message : string }) => {
metadata . set ( "progress" , 0 );
await childTask . triggerAndWait ({ message: "hello world" });
},
});
export const childTask = task ({
id: "child-task" ,
run : async ( payload : { message : string }) => {
// Update the parent task's metadata
metadata . parent . set ( "progress" , 0.5 );
// Update the root task's metadata (same as parent when depth is 1)
metadata . root . set ( "progress" , 0.5 );
},
});
All update methods are available on metadata.parent and metadata.root, and they support chaining:
metadata . parent
. set ( "progress" , 0.5 )
. append ( "logs" , "Step 1 complete" )
. increment ( "processedRows" , 1 );
Example: tracking progress across parallel child tasks
/trigger/csv-processing.ts
import { batch , metadata , schemaTask } from "@trigger.dev/sdk" ;
import { z } from "zod" ;
const CSVRow = z . object ({ id: z . string () });
const UploadedFileData = z . object ({ url: z . string () });
export const handleCSVRow = schemaTask ({
id: "handle-csv-row" ,
schema: CSVRow ,
run : async ( row , { ctx }) => {
// Increment counter and record run ID in the parent task
metadata . parent . increment ( "processedRows" , 1 ). append ( "rowRuns" , ctx . run . id );
return row ;
},
});
export const handleCSVUpload = schemaTask ({
id: "handle-csv-upload" ,
schema: UploadedFileData ,
run : async ( file ) => {
metadata . set ( "status" , "fetching" );
const rows = await fetch ( file . url ). then (( r ) => r . json ());
metadata . set ( "status" , "processing" ). set ( "totalRows" , rows . length );
const results = await batch . triggerAndWait < typeof handleCSVRow >(
rows . map (( row : { id : string }) => ({ id: "handle-csv-row" , payload: row }))
);
metadata . set ( "status" , "complete" );
return { file , results };
},
});
Combined with Realtime , this pattern gives you a live progress bar in your frontend.
Dashboard
Metadata for a run is displayed in the run details view in the Trigger.dev dashboard.
API
Retrieve a run’s metadata using runs.retrieve():
import { runs } from "@trigger.dev/sdk" ;
const run = await runs . retrieve ( "run_1234" );
console . log ( run . metadata );
See the runs.retrieve() reference for full details.
Metadata is loosely typed by default — it accepts any JSON-serializable object. Wrap the metadata API with a Zod schema for full type safety:
import { task , metadata } from "@trigger.dev/sdk" ;
import { z } from "zod" ;
const TaskMetadata = z . object ({
user: z . object ({
name: z . string (),
id: z . string (),
}),
startedAt: z . coerce . date (), // coerce string → Date on retrieval
});
type TaskMetadata = z . infer < typeof TaskMetadata >;
function getTypedMetadata () : TaskMetadata {
return TaskMetadata . parse ( metadata . current ());
}
export const myTask = task ({
id: "my-task" ,
run : async ( payload : { message : string }) => {
const meta = getTypedMetadata ();
console . log ( meta . user . name ); // string
console . log ( meta . startedAt ); // Date
},
});
Values like Date are serialized to strings when stored. Use z.coerce.date() or equivalent to
deserialize them on retrieval.
Constraints on the metadata value:
// ❌ Top-level arrays are not supported
await myTask . trigger ( payload , { metadata: [{ id: 1 }] });
// ❌ Plain strings are not supported
await myTask . trigger ( payload , { metadata: "some string" });
// ❌ Functions and class instances are not supported
await myTask . trigger ( payload , { metadata: { fn : () => {} } });
// ✅ Any JSON-serializable object
await myTask . trigger ( payload , { metadata: { user: { name: "Eric" }, date: new Date () } });
Size limit
The maximum metadata size is 256 KB . Exceeding this throws an error. If you are self-hosting, you can increase the limit with the TASK_RUN_METADATA_MAXIMUM_SIZE environment variable (value in bytes).