When a job needs to run on a timer, like data syncs, nightly cleanup, or report generation, Schedules let you configure timing and destinations outside your app code. The platform calls your endpoints on a cron schedule, tracks each delivery, and retries on failure.
Use the @agentuity/schedule standalone package to access this service from any Node.js or Bun app without the runtime.
There are two ways to run scheduled work:
- Cron routes are route handlers in your app, triggered on a schedule via HTTP POST. The schedule is defined in code and deployed with your project.
- Schedules are platform-managed resources. The platform calls your destinations (HTTP URLs or sandboxes) on schedule, with per-delivery tracking and automatic retries.
Use cron routes when you want in-process scheduling with direct access to agent context. Use Schedules when you need delivery tracking, multiple destination types, or want to manage job timing independently of deployments.
When to Use Schedules
| Approach | Best For |
|---|---|
| Schedules | Platform-managed recurring jobs with delivery tracking and multiple destination types |
| Cron Routes | In-process scheduled handlers with direct access to agent context |
| Queues | Event-driven async processing, not time-based |
Managing Schedules
| Context | Access | Details |
|---|---|---|
| Agents | ctx.schedule | See examples below |
| Routes | c.var.schedule | See Using in Routes |
| CLI | agentuity cloud schedule | Manage schedules from the command line |
| Web App | Web App | Create and inspect schedules in the UI |
Creating Schedules
Pass a name, an expression (standard five-field cron), and an optional array of destinations. The schedule and its destinations are created atomically: if any destination fails validation, the entire operation is rolled back.
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('ScheduleSetup', {
handler: async (ctx, input) => {
const result = await ctx.schedule.create({
name: 'Daily Report',
description: 'Generate and email daily reports', // optional
expression: '0 9 * * 1-5', // weekdays at 9am
destinations: [
{
type: 'url',
config: {
url: 'https://example.com/reports/generate',
method: 'POST',
headers: { 'Authorization': 'Bearer my-token' },
},
},
],
});
ctx.logger.info('Schedule created', {
id: result.schedule.id,
nextRun: result.schedule.due_date,
});
return { scheduleId: result.schedule.id };
},
});Common Cron Expressions
| Expression | Fires |
|---|---|
* * * * * | Every minute |
*/5 * * * * | Every 5 minutes |
0 * * * * | Every hour |
0 0 * * * | Daily at midnight |
0 9 * * 1-5 | Weekdays at 9am |
0 0 * * 1 | Weekly on Monday at midnight |
0 0 1 * * | Monthly on the 1st at midnight |
The server validates cron expressions on creation and update. due_date on the schedule reflects the next computed execution time.
Destination Types
Each schedule can have multiple destinations. When the schedule fires, the platform sends a request to each destination.
| Type | Config | Description |
|---|---|---|
url | { url, headers?, method? } | Sends an HTTP request to the URL on schedule |
sandbox | { sandbox_id, command? } | Runs a command in a sandbox on schedule |
URL Destination
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('WebhookScheduler', {
handler: async (ctx, input) => {
const { destination } = await ctx.schedule.createDestination(input.scheduleId, {
type: 'url',
config: {
url: 'https://example.com/webhook/sync',
method: 'POST',
headers: {
'Authorization': `Bearer ${input.apiToken}`,
'Content-Type': 'application/json',
},
},
});
ctx.logger.info('URL destination added', { destinationId: destination.id });
return { destinationId: destination.id };
},
});Sandbox Destination
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('SandboxScheduler', {
handler: async (ctx, input) => {
const { destination } = await ctx.schedule.createDestination(input.scheduleId, {
type: 'sandbox',
config: {
sandbox_id: input.sandboxId,
command: 'bun run src/run/sync.ts',
},
});
ctx.logger.info('Sandbox destination added', { destinationId: destination.id });
return { destinationId: destination.id };
},
});Listing and Updating Schedules
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('ScheduleManager', {
handler: async (ctx, input) => {
// List schedules with optional pagination
const { schedules, total } = await ctx.schedule.list({ limit: 20, offset: 0 });
ctx.logger.info('Schedules', { count: schedules.length, total });
// Get a specific schedule with its destinations
const { schedule, destinations } = await ctx.schedule.get(input.scheduleId);
ctx.logger.info('Next run', { dueDate: schedule.due_date });
// Update the cron expression: due_date is automatically recomputed
const { schedule: updated } = await ctx.schedule.update(input.scheduleId, {
expression: '0 0 * * *', // change to daily at midnight
});
return { nextRun: updated.due_date, destinationCount: destinations.length };
},
});Delivery Tracking
Each time a schedule fires, one delivery record is created per destination. Use listDeliveries to inspect delivery history and check for failures.
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('DeliveryInspector', {
handler: async (ctx, input) => {
const { deliveries } = await ctx.schedule.listDeliveries(input.scheduleId);
for (const delivery of deliveries) {
ctx.logger.info('Delivery record', {
date: delivery.date,
status: delivery.status, // 'pending' | 'success' | 'failed'
retries: delivery.retries, // number of retry attempts
error: delivery.error, // null on success
});
}
const failed = deliveries.filter(d => d.status === 'failed');
return { total: deliveries.length, failed: failed.length };
},
});Delivery Statuses
| Status | Description |
|---|---|
pending | Delivery is queued and has not been attempted yet |
success | The destination received the request and responded successfully |
failed | The delivery encountered an error; error field contains the reason |
The retries field on each delivery record tracks how many retry attempts were made before the final status was recorded.
Fetching a Specific Destination or Delivery
Two convenience methods let you look up a single record by ID without iterating manually.
getDestination(scheduleId, destinationId) fetches the schedule via get() and returns the matching destination:
const dest = await ctx.schedule.getDestination(input.scheduleId, input.destinationId);
ctx.logger.info('Destination type', { type: dest.type });getDelivery(scheduleId, deliveryId, params?) calls listDeliveries() and returns the matching record. Pass params to control the pagination window if the delivery may not be in the first page:
const delivery = await ctx.schedule.getDelivery(input.scheduleId, input.deliveryId);
ctx.logger.info('Delivery status', { status: delivery.status, retries: delivery.retries });Both methods throw if the ID is not found in the result set.
Removing Destinations and Schedules
import { createAgent } from '@agentuity/runtime';
const agent = createAgent('ScheduleCleaner', {
handler: async (ctx, input) => {
// Remove a single destination (keeps the schedule)
await ctx.schedule.deleteDestination(input.destinationId);
// Delete a schedule and all its destinations and delivery history
await ctx.schedule.delete(input.scheduleId);
return { deleted: true };
},
});delete() permanently removes the schedule, all its destinations, and all delivery history. This operation cannot be undone.
Using in Routes
Routes access the schedule service via c.var.schedule:
import { createRouter } from '@agentuity/runtime';
const router = createRouter();
router.post('/schedules', async (c) => {
const body = await c.req.json();
const result = await c.var.schedule.create({
name: body.name,
expression: body.expression,
destinations: body.destinations,
});
return c.json({ scheduleId: result.schedule.id, nextRun: result.schedule.due_date });
});
router.get('/schedules/:id/deliveries', async (c) => {
const scheduleId = c.req.param('id');
const { deliveries } = await c.var.schedule.listDeliveries(scheduleId);
return c.json({ deliveries });
});
export default router;Best Practices
- Use descriptive names: names like
"Daily User Report - EU"are easier to manage than"job-1"when listing schedules in the dashboard - Keep destinations idempotent: the platform may retry failed deliveries, so ensure your endpoints handle duplicate invocations safely
- Monitor delivery history: poll
listDeliveriesor check the dashboard to catch recurring failures before they accumulate - Remove unused destinations before changing schedules: deleting a destination does not affect other destinations on the same schedule, making partial updates safe
Next Steps
- Cron Routes: In-process scheduled handlers that run inside your deployed app
- Queues: Async, event-driven processing for work that doesn't need a fixed time trigger
- Sandbox: Isolated execution environments used as schedule destinations