Overview
The Jobs API allows you to dynamically create scheduled tasks from within your tools. Use this to defer work, schedule reminders, or automate recurring tasks.
import { Jobs } from 'lua-cli' ;
// Create a one-time reminder
const job = await Jobs . create ({
name: 'user-reminder' ,
metadata: { message: 'Team meeting in 10 minutes' },
schedule: {
type: 'once' ,
executeAt: new Date ( Date . now () + 600000 )
},
execute : async ( jobInstance ) => {
// ✅ Dynamic jobs automatically have user context!
const user = await jobInstance . user (); // No userId needed
await user . send ([{
type: 'text' ,
text: jobInstance . metadata . message
}]);
}
});
Automatic User Context: Dynamic jobs created from tools automatically know which user triggered them. Use jobInstance.user() to get the user - no userId required! This is different from pre-defined LuaJob which requires User.get(userId).
New in v3.0.0: Dynamically create jobs at runtime from within your tools. Pairs with the LuaJob class for pre-defined jobs.
Import
import { Jobs } from 'lua-cli' ;
// or
import { Jobs } from 'lua-cli/skill' ;
Capabilities
Dynamic Creation Create jobs on-demand from tools
One-time Tasks Schedule tasks for specific times
Recurring Jobs Set up intervals or cron patterns
Automatic User Context Jobs automatically know which user triggered them - use jobInstance.user()
Jobs API vs LuaJob
Understanding user access in different job types:
Feature Jobs API (Dynamic) LuaJob (Pre-defined) When Created Runtime, from tools At agent setup User Context ✅ Automatic ❌ None Get User jobInstance.user()User.get(userId)userId Needed? ❌ No (automatic) ✅ Yes (from metadata) Best For User-triggered tasks Regular scheduled tasks
Why the difference? Dynamic jobs are created during a user conversation, so they automatically capture that user’s context. Pre-defined jobs run on a schedule with no specific user, so you must explicitly provide a userId if you want to notify someone.
Methods
Jobs.create(config)
Creates a new scheduled job.
Returns: Promise<JobInstance>
Example:
const job = await Jobs . create ({
name: 'reminder-task' ,
description: 'Remind user about meeting' ,
metadata: { message: 'Don \' t forget the meeting!' },
schedule: {
type: 'once' ,
executeAt: new Date ( Date . now () + 3600000 )
},
execute : async ( jobInstance ) => {
const user = await jobInstance . user ();
await user . send ([{
type: 'text' ,
text: jobInstance . metadata . message
}]);
return { success: true };
}
});
Jobs.getJob(jobId)
Retrieves a job by ID.
Returns: Promise<JobInstance>
Example:
const job = await Jobs . getJob ( 'job_123' );
console . log ( job . name );
console . log ( job . activeVersion ?. schedule );
Jobs.getAll(options?)
Retrieves all jobs for the current agent.
Include dynamically created jobs (default: false)
Returns: Promise<JobInstance[]>
Example:
// Get all jobs including dynamically created ones
const jobs = await Jobs . getAll ({ includeDynamic: true });
for ( const job of jobs ) {
console . log ( ` ${ job . name } : ${ job . data . active ? 'active' : 'inactive' } ` );
}
// Find a specific job by name
const trackingJob = jobs . find ( j => j . name . startsWith ( 'track-game-' ));
if ( trackingJob ) {
await trackingJob . deactivate ();
}
Job Configuration
Required Fields
When/how often to run the job
Function that executes when job runs Signature: (job: JobInstance) => Promise<any>
Optional Fields
Job description for documentation
Data to pass to execute function Important: Use metadata to pass data - the execute function cannot access parent scope!
Maximum execution time in seconds Default: 60 (1 minute)
Retry configuration retry : {
maxAttempts : number ; // Max retry attempts
backoffSeconds ?: number ; // Seconds between retries (optional)
}
Whether to activate immediately Default: true
Schedule Types
Once (One-time execution)
schedule : {
type : 'once' ,
executeAt : Date // When to run
}
Examples:
// Run in 1 hour
schedule : {
type : 'once' ,
executeAt : new Date ( Date . now () + 3600000 )
}
// Run at specific time
schedule : {
type : 'once' ,
executeAt : new Date ( '2025-12-25T09:00:00Z' )
}
Interval (Recurring at fixed intervals)
schedule : {
type : 'interval' ,
seconds : number // Seconds between executions
}
Examples:
// Run every 5 minutes
schedule : {
type : 'interval' ,
seconds : 300
}
// Run every hour
schedule : {
type : 'interval' ,
seconds : 3600
}
Cron (Schedule with cron pattern)
schedule : {
type : 'cron' ,
expression : string // Cron expression
timezone ?: string // Optional timezone (e.g., 'America/New_York')
}
Examples:
// Every day at 9 AM
schedule : {
type : 'cron' ,
expression : '0 9 * * *'
}
// Every Monday at 8 AM EST
schedule : {
type : 'cron' ,
expression : '0 8 * * 1' ,
timezone : 'America/New_York'
}
// Every 15 minutes
schedule : {
type : 'cron' ,
expression : '*/15 * * * *'
}
Complete Examples
import { LuaTool , Jobs , JobInstance } from 'lua-cli/skill' ;
import { z } from 'zod' ;
export default class ReminderTool implements LuaTool {
name = 'set_reminder' ;
description = 'Set a reminder to notify user later' ;
inputSchema = z . object ({
message: z . string (). describe ( 'Reminder message' ) ,
minutes: z . number (). min ( 1 ). max ( 10080 ). describe ( 'Minutes from now' )
});
async execute ( input : z . infer < typeof this . inputSchema >) {
// Create job to notify user later
const job = await Jobs . create ({
name: `reminder- ${ Date . now () } ` ,
description: 'User reminder' ,
// ✅ Pass data via metadata (not parent scope!)
metadata: {
message: input . message ,
setAt: new Date (). toISOString ()
},
schedule: {
type: 'once' ,
executeAt: new Date ( Date . now () + input . minutes * 60000 )
},
execute : async ( jobInstance : JobInstance ) => {
const user = await jobInstance . user ();
const message = jobInstance . metadata . message ;
await user . send ([{
type: 'text' ,
text: `⏰ Reminder: ${ message } `
}]);
return {
success: true ,
deliveredAt: new Date (). toISOString ()
};
}
});
return {
success: true ,
message: `Reminder set for ${ input . minutes } minutes from now` ,
jobId: job . id ,
executeAt: job . activeVersion ?. schedule ?. executeAt
};
}
}
import { LuaTool , Jobs , JobInstance , Data } from 'lua-cli/skill' ;
import { z } from 'zod' ;
export default class ScheduleFollowupTool implements LuaTool {
name = 'schedule_followup' ;
description = 'Schedule a follow-up message for customer support' ;
inputSchema = z . object ({
ticketId: z . string () ,
followupHours: z . number (). min ( 1 ). max ( 168 ) ,
message: z . string ()
});
async execute ( input : z . infer < typeof this . inputSchema >) {
// Create job for follow-up
const job = await Jobs . create ({
name: `followup- ${ input . ticketId } ` ,
description: `Follow-up for ticket ${ input . ticketId } ` ,
metadata: {
ticketId: input . ticketId ,
message: input . message
},
schedule: {
type: 'once' ,
executeAt: new Date ( Date . now () + input . followupHours * 3600000 )
},
execute : async ( jobInstance : JobInstance ) => {
const user = await jobInstance . user ();
const { ticketId , message } = jobInstance . metadata ;
// Check if ticket is still open
const tickets = await Data . search ( 'tickets' , ticketId , 1 );
if ( tickets . data . length > 0 && tickets . data [ 0 ]. data . status === 'open' ) {
// Send follow-up
await user . send ([{
type: 'text' ,
text: `📋 Ticket # ${ ticketId } Follow-up: \n\n ${ message } `
}]);
// Update ticket
await Data . update ( 'tickets' , tickets . data [ 0 ]. id , {
... tickets . data [ 0 ]. data ,
followupSent: true ,
followupAt: new Date (). toISOString ()
});
return { success: true , sent: true };
}
return { success: true , sent: false , reason: 'Ticket closed' };
}
});
return {
success: true ,
message: `Follow-up scheduled for ${ input . followupHours } hours` ,
jobId: job . id
};
}
}
Recurring Report
import { LuaTool , Jobs , JobInstance , Products } from 'lua-cli/skill' ;
import { z } from 'zod' ;
export default class DailyReportTool implements LuaTool {
name = 'setup_daily_report' ;
description = 'Set up daily sales report' ;
inputSchema = z . object ({
timeOfDay: z . string (). regex ( / ^ ( [ 0-1 ] ? [ 0-9 ] | 2 [ 0-3 ] ) : [ 0-5 ][ 0-9 ] $ / )
. describe ( 'Time in HH:MM format (24-hour)' )
});
async execute ( input : z . infer < typeof this . inputSchema >) {
const [ hour , minute ] = input . timeOfDay . split ( ':' );
const job = await Jobs . create ({
name: 'daily-sales-report' ,
description: 'Daily sales summary' ,
metadata: {
reportTime: input . timeOfDay
},
schedule: {
type: 'cron' ,
expression: ` ${ minute } ${ hour } * * *` // Every day at specified time
},
execute : async ( jobInstance : JobInstance ) => {
// Get products sold today
const products = await Products . get ( 1 , 100 );
const totalValue = products . reduce (( sum , p ) => sum + ( p . price || 0 ), 0 );
const report = `📊 Daily Sales Report \n\n ` +
`Total Products: ${ products . length } \n ` +
`Catalog Value: $ ${ totalValue . toFixed ( 2 ) } \n ` +
`Report Time: ${ jobInstance . metadata . reportTime } ` ;
const user = await jobInstance . user ();
await user . send ([{ type: 'text' , text: report }]);
return {
success: true ,
productCount: products . length ,
totalValue
};
}
});
return {
success: true ,
message: `Daily report scheduled for ${ input . timeOfDay } ` ,
jobId: job . id
};
}
}
Jobs execute functions must be self-contained! They cannot access parent scope variables.
// ❌ WRONG - Accessing parent scope
async execute ( input : any ) {
const userMessage = input . message ; // This variable...
await Jobs . create ({
execute : async ( job ) => {
// ...is NOT available here! ❌
await user . send ( userMessage ); // Error: userMessage is not defined
}
});
}
// ✅ CORRECT - Using metadata
async execute ( input : any ) {
await Jobs . create ({
metadata: {
message: input . message // ✅ Pass via metadata
},
execute : async ( job ) => {
const message = job . metadata . message ; // ✅ Access from metadata
const user = await job . user ();
await user . send ([{ type: 'text' , text: message }]);
}
});
}
Why? Jobs are serialized, bundled, and executed in an isolated sandbox. They can’t access the parent function’s scope.
JobInstance Methods
The JobInstance passed to execute functions provides:
Properties
Property Type Description idstringUnique job identifier namestringJob name activeVersionJobVersionThe active version with schedule, timeout, etc. metadataobjectJob metadata dataJobFull job data including all versions
jobInstance.user()
Gets the user who triggered the job.
Returns: Promise<UserDataInstance>
Automatic User Context: This method is ONLY available for dynamic jobs created via Jobs.create(). The user context is automatically captured when the job is created from a tool. Pre-defined LuaJob must use User.get(userId) instead.
Example:
// In a tool creating a dynamic job
await Jobs . create ({
execute : async ( jobInstance ) => {
// ✅ Works! User context automatically available
const user = await jobInstance . user ();
await user . send ([{ type: 'text' , text: 'Job complete!' }]);
}
});
Comparison with LuaJob:
// ❌ This won't work in pre-defined LuaJob
const job = new LuaJob ({
execute : async ( job ) => {
const user = await job . user (); // ❌ No user() method!
}
});
// ✅ Use this for LuaJob instead
const job = new LuaJob ({
metadata: { userId: 'user_abc123' },
execute : async ( job ) => {
const user = await User . get ( job . metadata . userId ); // ✅ Works!
}
});
Access to the metadata passed during creation.
Example:
execute : async ( job ) => {
console . log ( job . metadata . customData );
}
Updates job metadata.
Example:
execute : async ( job ) => {
await job . updateMetadata ({
lastRun: new Date (). toISOString (),
runCount: ( job . metadata . runCount || 0 ) + 1
});
}
job.trigger(versionId?)
Manually triggers the job execution (ignores schedule). Uses the active version by default.
Parameters:
versionId (optional): Specific version to execute. Defaults to activeVersion.
Returns: Promise<JobExecution>
Example:
// Trigger with default active version
const execution = await job . trigger ();
console . log ( 'Execution ID:' , execution . id );
console . log ( 'Status:' , execution . status );
// Trigger with specific version
const execution = await job . trigger ( 'version_abc123' );
job.delete()
Deletes the job (or deactivates if it has versions).
Example:
execute : async ( job ) => {
// Do work...
// Delete one-time job after execution
await job . delete ();
}
job.activate()
Activates the job, enabling it to run on schedule.
Returns: Promise<JobInstance>
Example:
// Re-enable a paused job
const job = await Jobs . getJob ( 'job_123' );
await job . activate ();
console . log ( 'Job is now active' );
job.deactivate()
Deactivates the job, preventing it from running on schedule. Useful for jobs that should stop themselves.
Returns: Promise<JobInstance>
Example:
// Job stops itself when work is complete
execute : async ( job ) => {
const data = await fetchData ();
if ( data . isComplete ) {
// Stop the recurring job
await job . deactivate ();
return { action: 'completed' , stopped: true };
}
return { action: 'processed' , data };
}
Retry Configuration
await Jobs . create ({
name: 'important-task' ,
retry: {
maxAttempts: 3 ,
backoffSeconds: 60 // Wait 60s between retries
},
execute : async ( job ) => {
// If this throws, will retry up to 3 times
await criticalOperation ();
}
});
Timeout Configuration
await Jobs . create ({
name: 'long-running-task' ,
timeout: 300 , // 5 minutes max (in seconds)
execute : async ( job ) => {
// Will be terminated if exceeds 5 minutes
await longOperation ();
}
});
Best Practices
Give each job a descriptive, unique name name : `reminder- ${ Date . now () } `
name : `followup-ticket- ${ ticketId } `
✅ Handle Errors Gracefully
Implement error handling in execute function execute : async ( job ) => {
try {
await doWork ();
return { success: true };
} catch ( error ) {
return { success: false , error: error . message };
}
}
❌ Don't Access Parent Scope
Execute functions are isolated - use metadata! // ❌ This won't work
const data = input . value ;
execute : async ( job ) => {
console . log ( data ); // Error!
}
// ✅ Use metadata instead
metadata : { data : input . value },
execute : async ( job ) => {
console . log ( job . metadata . data ); // Works!
}
See Also