---
title: "Template: Recurring Job"
description: "Scheduler-triggered periodic workflow -- run a task on a schedule (daily, weekly, hourly, or custom cron)"
loadWhen: "Applying the recurring-job workflow template"
---

**Category:** Scheduling

**Platform Tools:** `scheduler` (create/manage schedule)

**Credentials Required:**

- None required (scheduler is a platform service, not an integration)

---

## What This Workflow Does

Two-part pattern for recurring jobs:

1. **Setup workflow** (`recurring-job-setup`): Creates a schedule entry that triggers the main job workflow on a recurring basis. Run once to activate.
2. **Main job workflow** (`recurring-job`): The actual work executed on each scheduled trigger.

The job workflow and setup workflow share a schedule key for idempotent schedule management.

---

## Input Schema (Setup)

```typescript
z.object({
  cronExpression: z.string(),           // Cron expression (e.g., '0 9 * * 1-5' for weekdays at 9am)
  timezone: z.string().optional(),      // Timezone (default: 'UTC')
  jobInput: z.record(z.unknown()).optional(), // Input to pass to each job execution
})
```

## Input Schema (Job)

```typescript
z.object({
  scheduledAt: z.string(),              // ISO timestamp of the scheduled trigger
  jobInput: z.record(z.unknown()).optional(), // Passed through from schedule creation
})
```

## Output Schema (Job)

```typescript
z.object({
  completed: z.boolean(),
  processedAt: z.string(),
  summary: z.string(),
})
```

---

## Workflow Code Pattern

```typescript
import type { WorkflowDefinition } from '@elevasis/sdk'
import { platform } from '@elevasis/sdk/worker'
import { z } from 'zod'

// Setup workflow -- run once to create the schedule
export const recurringJobSetup: WorkflowDefinition = {
  config: {
    resourceId: 'recurring-job-setup',
    name: 'Recurring Job Setup',
    type: 'workflow',
    description: 'Creates or updates the schedule for the recurring job',
    version: '1.0.0',
    status: 'dev',
  },
  contract: {
    inputSchema: z.object({
      cronExpression: z.string(),
      timezone: z.string().optional(),
      jobInput: z.record(z.unknown()).optional(),
    }),
    outputSchema: z.object({
      scheduleId: z.string(),
      active: z.boolean(),
    }),
  },
  steps: {
    createSchedule: {
      id: 'createSchedule',
      name: 'Create Schedule',
      description: 'Set up the recurring schedule',
      inputSchema: z.object({ cronExpression: z.string(), timezone: z.string().optional(), jobInput: z.record(z.unknown()).optional() }),
      outputSchema: z.object({ scheduleId: z.string(), active: z.boolean() }),
      handler: async (input, context) => {
        const { cronExpression, timezone, jobInput } = input as { cronExpression: string; timezone?: string; jobInput?: Record<string, unknown> }

        const result = await platform.call({
          tool: 'scheduler',
          method: 'createSchedule',
          params: {
            resourceId: 'recurring-job',
            cronExpression,
            timezone: timezone ?? 'UTC',
            input: { scheduledAt: new Date().toISOString(), jobInput: jobInput ?? {} },
            idempotencyKey: 'recurring-job-schedule',
          },
        }) as { id: string }

        context.logger.info('Schedule created', { scheduleId: result.id, cronExpression })
        return { scheduleId: result.id, active: true }
      },
      next: null,
    },
  },
  entryPoint: 'createSchedule',
}

// Main job workflow -- triggered by the scheduler on each run
export const recurringJob: WorkflowDefinition = {
  config: {
    resourceId: 'recurring-job',
    name: 'Recurring Job',
    type: 'workflow',
    description: 'Executes on each scheduled trigger',
    version: '1.0.0',
    status: 'dev',
  },
  contract: {
    inputSchema: z.object({
      scheduledAt: z.string(),
      jobInput: z.record(z.unknown()).optional(),
    }),
    outputSchema: z.object({
      completed: z.boolean(),
      processedAt: z.string(),
      summary: z.string(),
    }),
  },
  steps: {
    run: {
      id: 'run',
      name: 'Run Job',
      description: 'Execute the recurring job logic',
      inputSchema: z.object({ scheduledAt: z.string(), jobInput: z.record(z.unknown()).optional() }),
      outputSchema: z.object({ completed: z.boolean(), processedAt: z.string(), summary: z.string() }),
      handler: async (input, context) => {
        const { scheduledAt, jobInput } = input as { scheduledAt: string; jobInput?: Record<string, unknown> }

        context.logger.info('Recurring job started', { scheduledAt, jobInput })

        // === REPLACE THIS SECTION WITH THE ACTUAL JOB LOGIC ===
        // Example: fetch data, process it, send a report
        const summary = `Job completed at ${new Date().toISOString()}`
        // ======================================================

        return { completed: true, processedAt: new Date().toISOString(), summary }
      },
      next: null,
    },
  },
  entryPoint: 'run',
}
```

---

## Common Cron Expressions

| Schedule | Expression | Notes |
| --- | --- | --- |
| Every day at 9am (weekdays) | `0 9 * * 1-5` | Monday-Friday |
| Every day at midnight | `0 0 * * *` | UTC unless timezone set |
| Every hour | `0 * * * *` | On the hour |
| Every Monday at 8am | `0 8 * * 1` | |
| First of each month | `0 0 1 * *` | Midnight UTC |
| Every 15 minutes | `*/15 * * * *` | Frequent polling |

---

## Adaptation Notes

- **Job logic:** Replace the placeholder comment in the `run` handler with the actual job logic. Ask the user what the job should do before generating the full workflow.
- **Both workflows needed:** Remind the user to add both `recurringJob` and `recurringJobSetup` to their `src/index.ts` registry. Run `setup` once to activate; only `recurring-job` runs automatically thereafter.
- **Idempotency key:** The `recurring-job-schedule` key ensures re-running setup does not create duplicate schedules.
- **Timezone:** Always ask the user their preferred timezone. Defaults to UTC which may cause unexpected run times.

---

**Last Updated:** 2026-02-26
