> ## 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.

# Jobs

> Dynamic jobs created at run time, and the JobInstance handle every job receives

`Jobs` creates, fetches, and lists dynamic jobs: scheduled functions your code registers at run time, such as a reminder for the end user who is talking to the agent. A dynamic job runs as the end user who created it, so `job.user()` works inside it. Jobs you ship with the agent are declared with [`LuaJob`](/reference/sdk/luajob); both kinds receive the `JobInstance` documented here. Available in tools, jobs, webhooks, triggers, and processors.

*Verified against lua-cli 3.33.0.*

```ts theme={null}
import { Jobs } from 'lua-cli';
```

## Quick example

```ts theme={null}
import { Jobs } from 'lua-cli';

const job = await Jobs.create({
  name: 'meeting-reminder',
  schedule: { type: 'once', executeAt: new Date(Date.now() + 10 * 60 * 1000) },
  metadata: { text: 'Your meeting starts in 10 minutes.' },
  execute: async (job) => {
    const user = await job.user();
    await user.send([{ type: 'text', text: job.metadata.text }]);
  },
});
console.log(job.id, job.activeVersion?.schedule);
```

## Methods

### create()

Creates a dynamic job with version `1.0.0` and, by default, activates it.

```ts theme={null}
Jobs.create(config: {
  name: string;
  description?: string;
  schedule: JobSchedule;
  execute: (job: JobInstance) => Promise<any>;
  timeout?: number;
  retry?: { maxAttempts: number; backoffSeconds?: number };
  metadata?: Record<string, any>;
  activate?: boolean;
}): Promise<JobInstance>
```

<ParamField path="name" type="string" required>
  Base name. The platform appends a timestamp so repeated creations never collide: `<name> - <ms since epoch>` in deployed agents, `<name>_<ms since epoch>` in `lua test`. Match on a prefix (`job.name.startsWith('meeting-reminder')`), never on equality.
</ParamField>

<ParamField path="description" type="string">
  Free text stored with the job.
</ParamField>

<ParamField path="schedule" type="JobSchedule" required>
  One of `{ type: 'cron', expression, timezone? }`, `{ type: 'once', executeAt: Date | string }`, or `{ type: 'interval', seconds }`. Typed `any` on this method; [`JobSchedule`](/reference/sdk/luajob) is the shape the platform accepts.
</ParamField>

<ParamField path="execute" type="(job: JobInstance) => Promise<any>" required>
  Runs on every occurrence with the job's handle. The function is stored as its source text, so variables from the enclosing scope are not captured; pass values through `metadata` and read them from `job.metadata`. When created from deployed tool, job, webhook, or processor code, the function runs inside that code's compiled bundle and module-level imports resolve. In `lua test` it is stored on its own, so use only `metadata` and the runtime objects (`User`, `Data`, `Channels`, and the others) inside it.
</ParamField>

<ParamField path="timeout" type="number">
  Maximum run time in seconds, from 1 to 600.
</ParamField>

<ParamField path="retry" type="{ maxAttempts: number; backoffSeconds?: number }">
  Retries after a failed attempt, a timed-out one included, while `maxAttempts` allows. Each retry waits a fixed `backoffSeconds` (default 60; `0` is treated as 60), with no jitter, and the platform stops after 10 attempts whatever `maxAttempts` says.
</ParamField>

<ParamField path="metadata" type="Record<string, any>">
  JSON stored with the job and available as `job.metadata`.
</ParamField>

<ParamField path="activate" type="boolean" default={true}>
  Whether the job starts running on its schedule at once.
</ParamField>

**Returns** — the created job as a [`JobInstance`](#jobinstance).

**Example**

```ts theme={null}
import { Data, Jobs } from 'lua-cli';

await Jobs.create({
  name: 'nightly-sync',
  description: 'Refresh the cached account list',
  schedule: { type: 'cron', expression: '0 2 * * *', timezone: 'Europe/London' },
  timeout: 120,
  retry: { maxAttempts: 3, backoffSeconds: 60 },
  execute: async (job) => {
    const accounts = await Data.get('accounts', { status: 'active' }, 1, 100);
    await job.updateMetadata({ lastCount: accounts.pagination.totalCount });
  },
});
```

**Errors** — `Failed to create job` (`lua test`) or `Failed to create job: <reason>` (deployed) when the platform rejects the configuration, for example a `timeout` outside 1 to 600.

### getJob()

Returns one job by id.

```ts theme={null}
Jobs.getJob(jobId: string): Promise<JobInstance>
```

**Returns** — the job as a [`JobInstance`](#jobinstance). An unknown id throws; there is no `null` return.

**Example**

```ts theme={null}
import { Jobs } from 'lua-cli';

const job = await Jobs.getJob('job_abc123');
console.log(job.name, job.data.active, job.activeVersion?.schedule);
```

**Errors** — `Failed to get job` (`lua test`) or `Failed to get job: <reason>` (deployed).

### getAll()

Returns the agent's jobs.

```ts theme={null}
Jobs.getAll(options?: { includeDynamic?: boolean }): Promise<JobInstance[]>
```

<ParamField path="options.includeDynamic" type="boolean" default={false}>
  `false` lists only jobs declared with `LuaJob`; `true` adds jobs created with `Jobs.create()`.
</ParamField>

**Returns** — an array of [`JobInstance`](#jobinstance).

**Example**

```ts theme={null}
import { Jobs } from 'lua-cli';

const jobs = await Jobs.getAll({ includeDynamic: true });
for (const job of jobs.filter((j) => j.name.startsWith('meeting-reminder'))) {
  await job.deactivate();
}
```

**Errors** — `Failed to get all jobs`.

## JobInstance

The handle `create()`, `getJob()`, and `getAll()` return, and the argument every job's `execute` receives.

<ResponseField name="id" type="string">Job id.</ResponseField>
<ResponseField name="name" type="string">The stored name, including the platform's timestamp suffix for dynamic jobs.</ResponseField>

<ResponseField name="activeVersion" type="JobVersion">
  The active version: `id`, `version`, `schedule`, `timeout`, `retry`, `metadata`, `active`, `createdAt`, and `updatedAt`. Absent when no version is active.
</ResponseField>

<ResponseField name="metadata" type="Record<string, any>">The job's metadata.</ResponseField>

<ResponseField name="data" type="Job">
  The full record: `id`, `name`, `description`, `agentId`, `active`, `status` (`active`, `paused`, `failed`, or `inactive`), `activeVersionId`, `versions`, `dynamic`, `userId`, `lastRunAt`, `nextRunAt`, `lastExecution`, `createdAt`, and `updatedAt`.
</ResponseField>

<ResponseField name="execution" type="{ executionId: string; attempt: number; occurrenceId: string; scheduledTime?: string }">
  Attempt information for the current run, set on every deployed run; see [Retries and idempotency](#retries-and-idempotency).
</ResponseField>

### updateMetadata()

Merges fields into `metadata` locally and stores the merged object.

```ts theme={null}
job.updateMetadata(metadata: Record<string, any>): Promise<void>
```

**Errors** — `Failed to update job metadata`, or the platform's message.

### delete()

Deletes the job, or deactivates it when it has versions.

```ts theme={null}
job.delete(): Promise<void>
```

**Example**

```ts theme={null}
import { Jobs } from 'lua-cli';

await Jobs.create({
  name: 'one-off-cleanup',
  schedule: { type: 'once', executeAt: new Date(Date.now() + 60 * 1000) },
  execute: async (job) => {
    await job.updateMetadata({ ranAt: new Date().toISOString() });
    await job.delete();
  },
});
```

**Errors** — `Failed to delete job`, or the platform's message.

### user()

Returns the end user recorded on the job: the end user who created a dynamic job, or the developer who pushed a job declared with `LuaJob`.

```ts theme={null}
job.user(): Promise<UserDataInstance>
```

**Returns** — the recorded end user as a [`UserDataInstance`](/reference/sdk/user#userdatainstance). To act on a specific end user from a declared job, use `User.get(id)` instead.

**Errors** — `User API not initialized` when the job record carries no `userId`.

### trigger()

Runs the job immediately, outside its schedule.

```ts theme={null}
job.trigger(versionId?: string): Promise<JobExecution>
```

<ParamField path="versionId" type="string">A version to run. Defaults to the active version.</ParamField>

**Returns**

<ResponseField name="execution" type="JobExecution">
  `id`, `jobId`, `versionId`, `status`, `startedAt`, and, once finished, `completedAt`, `duration`, `result`, `error`, and `retryCount`. `status` is one of `pending`, `claimed`, `running`, `cancellation_requested`, `completed`, `failed`, `timeout`, `killed`, `cancelled`, `abandoned`, or `reaped`.
</ResponseField>

Three of those statuses are set by the platform rather than by your code. `killed` is an attempt the platform closed because it ran past its wall-clock budget and grace period; it counts as a failure and is retried under `retry`. `reaped` is an attempt whose worker stopped renewing its lease, so the worker is gone; it is not retried, and the next scheduled occurrence runs as normal. `abandoned` is an attempt fenced by a forced cancel after a cooperative cancel went unanswered for 10 minutes; it is not retried, and a late result from the run no longer changes the record. A timed-out attempt is recorded as `failed`, not `timeout`.

**Errors** — `Failed to trigger job`, or the platform's message.

### activate()

Lets the job run on its schedule.

```ts theme={null}
job.activate(): Promise<JobInstance>
```

**Errors** — `Failed to activate job`, or the platform's message.

### deactivate()

Stops the job from running on its schedule without deleting it.

```ts theme={null}
job.deactivate(): Promise<JobInstance>
```

**Example**

```ts theme={null}
import { Jobs } from 'lua-cli';

await Jobs.create({
  name: 'poll-until-done',
  schedule: { type: 'interval', seconds: 300 },
  metadata: { remaining: 3 },
  execute: async (job) => {
    const remaining = job.metadata.remaining - 1;
    await job.updateMetadata({ remaining });
    if (remaining <= 0) await job.deactivate();
  },
});
```

**Errors** — `Failed to deactivate job`, or the platform's message.

### toJSON()

Returns the full record with `id`, `name`, `activeVersion`, and `metadata` merged in.

## Retries and idempotency

A job runs at least once per occurrence. A failed attempt, a timed-out one included, is retried according to `retry` while `maxAttempts` allows: each retry waits a fixed `backoffSeconds` (default 60), with no jitter, and the platform stops after 10 attempts whatever `maxAttempts` says. A single occurrence can therefore run more than once. The running handle's `execution` tells the attempts apart: `occurrenceId` stays constant across every attempt of one occurrence, `executionId` is minted per attempt, `attempt` starts at 1, and `scheduledTime` is the schedule slot for scheduled runs only.

<Info>
  Deployed runs only. `execution` is set on the handle every deployed run receives, is `undefined` in `lua test`, and is not declared on `JobInstance` in lua-cli 3.33.0, so read it through a cast and treat it as optional.
</Info>

```ts theme={null}
import { Data, Jobs } from 'lua-cli';

type JobExecutionInfo = { executionId: string; attempt: number; occurrenceId: string; scheduledTime?: string };

await Jobs.create({
  name: 'charge-renewals',
  schedule: { type: 'cron', expression: '0 6 * * *' },
  retry: { maxAttempts: 3, backoffSeconds: 120 },
  execute: async (job) => {
    const execution = (job as unknown as { execution?: JobExecutionInfo }).execution;
    const key = execution?.occurrenceId ?? job.id;

    // A retry must not repeat the side effect an earlier attempt completed
    const done = await Data.get('processed_occurrences', { key });
    if (done.data.length > 0) return;

    await job.updateMetadata({ lastChargeStartedAt: new Date().toISOString() });
    await Data.create('processed_occurrences', { key });
  },
});
```

## Types

`Jobs`, `JobInstance`, and `JobSchedule` are exported. `Job`, `JobVersion`, and `JobExecution` are not; name them from the handle.

```ts theme={null}
import { Jobs } from 'lua-cli';
import type { JobInstance, JobSchedule } from 'lua-cli';

type JobRecord = JobInstance['data'];
type JobExecutionRecord = Awaited<ReturnType<JobInstance['trigger']>>;

export const nightly: JobSchedule = { type: 'cron', expression: '0 2 * * *', timezone: 'UTC' };

export async function activeJobs(): Promise<JobRecord[]> {
  const jobs = await Jobs.getAll({ includeDynamic: true });
  return jobs.map((job) => job.data).filter((job) => job.active);
}

export function succeeded(execution: JobExecutionRecord): boolean {
  return execution.status === 'completed';
}
```

## See also

* [`LuaJob`](/reference/sdk/luajob) — jobs shipped with the agent, and the `JobSchedule` type
* [About jobs](/concepts/jobs) — dynamic jobs versus `LuaJob`, schedules, timeouts, and delivery
* [Schedule a recurring job](/build/schedule-a-job) — how-to
* [`User`](/reference/sdk/user) — what `job.user()` returns
* [`lua jobs`](/reference/cli/jobs) — inspect, trigger, and deactivate jobs from the CLI
