Skip to main content

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:
FeatureJobs API (Dynamic)LuaJob (Pre-defined)
When CreatedRuntime, from toolsAt agent setup
User Context✅ Automatic❌ None
Get UserjobInstance.user()User.get(userId)
userId Needed?❌ No (automatic)✅ Yes (from metadata)
Best ForUser-triggered tasksRegular 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.
config
JobConfig
required
Job configuration object
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.
jobId
string
required
Job ID to retrieve
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.
options.includeDynamic
boolean
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

name
string
required
Unique job name
schedule
JobSchedule
required
When/how often to run the job
execute
function
required
Function that executes when job runsSignature: (job: JobInstance) => Promise<any>

Optional Fields

description
string
Job description for documentation
metadata
object
Data to pass to execute functionImportant: Use metadata to pass data - the execute function cannot access parent scope!
timeout
number
Maximum execution time in secondsDefault: 60 (1 minute)
retry
object
Retry configuration
retry: {
  maxAttempts: number;     // Max retry attempts
  backoffSeconds?: number; // Seconds between retries (optional)
}
activate
boolean
Whether to activate immediatelyDefault: 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

Reminder Tool

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
    };
  }
}

Follow-up Tool

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
    };
  }
}

Important: Metadata Pattern

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

PropertyTypeDescription
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!
  }
});

job.metadata

Access to the metadata passed during creation. Example:
execute: async (job) => {
  console.log(job.metadata.customData);
}

job.updateMetadata(data)

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}`
Always use metadata to pass data to execute function
metadata: {
  userId: input.userId,
  message: input.message,
  timestamp: new Date().toISOString()
}
Implement error handling in execute function
execute: async (job) => {
  try {
    await doWork();
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
}
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