> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Schedule a recurring job

> Run a function on a cron schedule with LuaJob, test it locally, and release it

After this guide, your agent runs a function every morning at 09:00 in a timezone you choose, with no end user involved. A declared [job](/concepts/jobs) is for agent-wide schedules; for a reminder tied to one end user, create a dynamic job from a tool instead ([Create a job from a tool](#create-a-job-from-a-tool)).

*Verified against lua-cli 3.33.0.*

**Before you begin**

* A project created with `lua init` and signed in with `lua auth configure` ([Install and sign in](/get-started/install)).
* Any secret the job needs stored with `lua env production -k <KEY> -v <value>` ([About environments](/concepts/environments)).

<Steps>
  <Step title="Create the job">
    Add a file under `src/jobs/`. `schedule` takes a cron `expression` and an optional IANA `timezone`; `timeout` is in seconds, from 1 to 600 (default 300); `retry` gives a failed or timed-out attempt more tries: each retry waits a fixed `backoffSeconds`, and the platform stops after 10 attempts ([Jobs reference](/reference/sdk/jobs)).

    ```ts src/jobs/DailyDigestJob.ts theme={null}
    import { LuaJob, Data } from 'lua-cli';

    export default new LuaJob({
      name: 'daily-digest',
      description: 'Count the orders still marked placed and record the total',
      schedule: { type: 'cron', expression: '0 9 * * *', timezone: 'Europe/London' },
      timeout: 120,
      retry: { maxAttempts: 3, backoffSeconds: 60 },
      async execute() {
        const orders = await Data.get('orders', { status: 'placed' }, 1, 100);
        return { placed: orders.pagination.totalCount, ranAt: new Date().toISOString() };
      },
    });
    ```

    A job runs outside any conversation: `User.get()` with no argument has nobody to return, and `job.user()` resolves to the user recorded on the job, which for a declared job is never an end user. Reach an end user with `User.get(userId)` and an ID you stored earlier ([Execution contexts](/concepts/execution-contexts)).
  </Step>

  <Step title="Register it on the agent">
    Only primitives referenced from `LuaAgent` are compiled.

    ```ts src/index.ts highlight={7} theme={null}
    import { LuaAgent } from 'lua-cli';
    import dailyDigest from './jobs/DailyDigestJob';

    export default new LuaAgent({
      name: 'shop-assistant',
      persona: 'You help customers of Acme with orders.',
      jobs: [dailyDigest],
    });
    ```
  </Step>

  <Step title="Run it once locally">
    `lua test job` compiles the project and runs `execute` on your machine straight away, ignoring the schedule; the platform APIs it calls are real.

    ```bash theme={null}
    lua test job --name daily-digest --ci
    ```

    ```text Output theme={null}
    ✅ Selected job: daily-digest
    🚀 Executing job: daily-digest...
    📅 Schedule: Cron: 0 9 * * * (Europe/London)
    ✅ Job execution successful!

    Job returned: Object — fields: placed, ranAt
    Output:
    { placed: 0, ranAt: '2026-09-12T17:52:23.516Z' }
    ```
  </Step>

  <Step title="Release it">
    `lua push` uploads a version and changes nothing for end users; `lua version create` snapshots the agent; `lua version promote <n>` makes that snapshot live and is also the rollback path ([Release an agent to production](/ship/releasing)).

    ```bash theme={null}
    lua push all --ci --force
    lua version create --ci -m "Add daily digest job"
    lua version promote <n>
    ```

    `lua version create` prints `✓ Created v<n> (staged)`; in a script, `n=$(lua version list --limit 1 --json --ci | jq -r '.[0].version')` then `lua version promote "$n"`. `lua push all` exits 0 even when a primitive fails; check its output for `component(s) failed to push`.

    <Warning>
      From the promote on, the job fires at its next scheduled tick in production. Pause it with `lua jobs deactivate -i daily-digest` and resume it with `lua jobs activate -i daily-digest`.
    </Warning>
  </Step>

  <Step title="Verify">
    Run the job immediately instead of waiting for 09:00, then read its history.

    ```bash theme={null}
    lua jobs trigger -i daily-digest
    lua jobs history -i daily-digest
    ```

    `trigger` prints the execution ID and leaves the schedule unchanged. `history` prints `📊 Execution History for daily-digest` and the last 20 runs, newest first, each as a status line such as `✅ COMPLETED`, then `Started`, `Completed`, `Duration`, and `Result:` with the first 100 characters of what `execute` returned. `lua logs --type job --name daily-digest --limit 5` shows what the run logged.
  </Step>
</Steps>

## Options you may need

### Run once or at an interval

Two other schedule shapes exist: `{ type: 'once', executeAt: '2026-10-01T09:00:00Z' }` runs a single time at a `Date` or ISO string, and `{ type: 'interval', seconds: 300 }` runs every fixed period.

### Create a job from a tool

`Jobs.create` makes a job while the agent runs, stores it as version `1.0.0`, and starts it unless you pass `activate: false`. Its `execute` function is sent to the platform as source text, so it cannot read variables from the tool around it: put what it needs in `metadata` and read `job.metadata` inside. A dynamic job remembers the end user who was in the conversation, and `job.user()` returns them.

```ts src/skills/tools/RemindMeTool.ts theme={null}
import { LuaTool, Jobs } from 'lua-cli';
import { z } from 'zod';

export default class RemindMeTool implements LuaTool {
  name = 'remind_me';
  description = 'Send the current user a reminder after a number of minutes';
  inputSchema = z.object({
    text: z.string().describe('What to remind the user about'),
    inMinutes: z.number().int().positive(),
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const executeAt = new Date(Date.now() + input.inMinutes * 60_000);
    const job = await Jobs.create({
      name: 'reminder',
      description: 'One-off reminder requested in chat',
      schedule: { type: 'once', executeAt },
      // Only metadata reaches the job; `input` is not available inside execute.
      metadata: { text: input.text },
      async execute(job) {
        const user = await job.user();
        await user.send([{ type: 'text', text: `Reminder: ${job.metadata.text}` }]);
        await job.delete();
      },
    });
    return { jobId: job.id, executeAt: executeAt.toISOString() };
  }
}
```

The stored name is `reminder - <timestamp>` (`reminder_<timestamp>` when the tool runs under `lua test`), so every call creates a distinct job, and `Jobs.getAll()` includes dynamic jobs only with `{ includeDynamic: true }`.

### Deploy the job on its own

`lua deploy job` is the single-primitive shortcut: it creates and promotes an agent version scoped to that job, so the job goes live immediately and the version appears in `lua version list` like any other.

```bash theme={null}
lua push job --name daily-digest --ci --force
lua deploy job --name daily-digest --set-version latest --force
```

## If it isn't working

<AccordionGroup>
  <Accordion title="No jobs found in compiled output.">
    **Cause** `lua test job` only sees jobs registered on `LuaAgent.jobs`. **Fix** Import the job in `src/index.ts`, add it to `jobs`, and run the test again.
  </Accordion>

  <Accordion title="The job ran but no end user received anything">
    **Cause** A declared job has no current user, so `User.get()` without an argument has nothing to return. **Fix** Store the user ID when you learn it and call `User.get(userId)` inside the job; see [Send proactive messages](/build/send-proactive-messages).
  </Accordion>

  <Accordion title="A dynamic job cannot see the tool's input">
    **Cause** `Jobs.create` serializes `execute` to text and sends nothing it closed over. **Fix** Pass the values in `metadata` and read `job.metadata` inside `execute`.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="About jobs" href="/concepts/jobs">Declared and dynamic jobs, schedules, timeouts, and when a workflow fits better.</Card>
  <Card title="LuaJob reference" href="/reference/sdk/luajob">Every schedule type, limit, and retry field.</Card>
  <Card title="Jobs reference" href="/reference/sdk/jobs">`Jobs.create`, `Jobs.getAll`, and `JobInstance`.</Card>
  <Card title="lua jobs" href="/reference/cli/jobs">Trigger, history, activate, deactivate, and deploy.</Card>
</Columns>
