---
title: "Recurring Jobs"
description: "Legacy compatibility reference for scheduled automations, cron execution, organization scope, and advanced job fields."
---

# Recurring Jobs

A **recurring job** is the legacy name for a **scheduled automation**: a prompt
that runs on a cron schedule. New product guidance uses
[Automations](/docs/automations) as the umbrella, with **Scheduled** and
**Event** as trigger types.

The Jobs name remains in file paths, APIs, and tools for compatibility.
`manage-jobs` is schedule-only; new Scheduled and Event work uses
`manage-automations`, including organization scope, model overrides, and MCP
allowlists. This page remains the compatibility and scheduler reference.

Jobs live in [Agent Resources](/docs/agent-resources) at `jobs/<name>.md` — just a Markdown file with YAML frontmatter. No registration, no wiring. Drop the file in and the framework picks it up.

<Callout id="doc-block-rj1" tone="info">

**Creating new scheduled work?** Use the
[Automations workflow](/docs/automations#creating). Continue here when you need
the legacy `manage-jobs` API, delivery metadata, or scheduler internals.

</Callout>

## A job file {#job-file}

<AnnotatedCode
  id="doc-block-3pl82t"
  title="jobs/morning-digest.md"
  filename="jobs/morning-digest.md"
  language="markdown"
  code={
    '---\nschedule: "0 7 * * *"\nenabled: true\nrunAs: creator\n---\n\n# Morning digest\n\nSummarize the emails received overnight. Group by sender domain.\nPin the top 3 threads that look like they need a reply today to the\n"Needs reply" label. Draft replies for any that are obvious.'
  }
  annotations={[
    {
      lines: "2",
      label: "When",
      note: "Standard 5-field cron — `0 7 * * *` is every day at 07:00.",
    },
    {
      lines: "3",
      label: "Pause switch",
      note: "Flip to `false` to stop the job without deleting it.",
    },
    {
      lines: "4",
      label: "Identity",
      note: "`creator` runs with the owner's identity and `ANTHROPIC_API_KEY`; `shared` uses the org's key.",
    },
    {
      lines: "7-12",
      label: "The prompt",
      note: "The body is just a prompt — the agent runs it at each firing with all its normal tools and agent context.",
    },
  ]}
/>

That's it. The body is a prompt the agent runs at each scheduled firing. The agent has access to all the same tools and agent context it has in an interactive chat — actions, skills, memory, connected MCP servers, sub-agents.

## Frontmatter {#frontmatter}

| Field        | Type                          | Default      | Description                                                                                            |
| ------------ | ----------------------------- | ------------ | ------------------------------------------------------------------------------------------------------ |
| `schedule`   | cron expression               | _(required)_ | Standard 5-field cron. `"0 7 * * *"` = every day at 07:00; `"0 */4 * * *"` = every 4 hours.            |
| `enabled`    | boolean                       | `true`       | Flip to `false` to pause without deleting the job.                                                     |
| `runAs`      | `"creator"` \| `"shared"`     | `"creator"`  | `"creator"` runs with the job owner's identity and `ANTHROPIC_API_KEY`. `"shared"` uses the org's key. |
| `createdBy`  | email                         | _(auto)_     | Populated when the job is created through the Agent Resources UI or by the agent.                      |
| `orgId`      | string                        | _(auto)_     | Org scope; inherited from the creator's active org.                                                    |
| `appId`      | string                        | _(auto)_     | The app whose scheduler runs the job. A scheduler skips a job that names another app.                  |
| `lastRun`    | ISO timestamp                 | _(managed)_  | Written by the scheduler after each run.                                                               |
| `lastStatus` | `"success"` \| `"error"` \| … | _(managed)_  | Latest outcome.                                                                                        |
| `lastError`  | string                        | _(managed)_  | Error message if the last run failed.                                                                  |
| `nextRun`    | ISO timestamp                 | _(managed)_  | Computed from `schedule`; used by the scheduler to decide when to fire next.                           |
| `model`      | model id                      | _(default)_  | Optional model override for this automation.                                                           |
| `mcpTools`   | array of MCP tool names       | _(none)_     | Optional explicit allowlist such as `mcp__server__tool`; credentials stay in the connected MCP grant.  |

The `last*` and `nextRun` fields are written by the scheduler. You can read them to see the history, but don't edit them by hand — the next run will overwrite.

## Connecting MCP tools to a job {#connected-mcps}

Recurring jobs can use remote or local MCP tools that are already connected to
the app. Ask the agent to discover the exact names with `tool-search`, then
bind only the tools that job needs with `mcpTools`:

```md
---
schedule: "0 * * * *"
enabled: true
mcpTools:
  ["mcp__meeting-notes__list_recordings", "mcp__meeting-notes__get_transcript"]
---

Find new recordings since the last successful run. Extract only actionable
follow-ups, then call the app's bounded import action once with stable source
and external ids, a short citation, and the recording URL. Do not import the
full transcript.
```

The automation stores tool names, not MCP URLs, access tokens, or OAuth refresh
tokens. At run time, the shared background runner resolves the connected grant
in the creator and organization request context and exposes only the
allowlisted tools. A missing configured tool fails the run rather than widening
access. Keep the list narrow, and use an idempotent app action for the final
import so overlapping runs do not create duplicates. See
[MCP Clients](/docs/mcp-clients) for connection and OAuth setup.

## Cron syntax {#cron}

Standard 5-field cron (minute, hour, day-of-month, month, day-of-week):

| Cron           | Meaning                  |
| -------------- | ------------------------ |
| `*/5 * * * *`  | Every 5 minutes          |
| `0 * * * *`    | Every hour on the hour   |
| `0 */4 * * *`  | Every 4 hours            |
| `0 7 * * *`    | Every day at 07:00       |
| `0 9 * * 1`    | Every Monday at 09:00    |
| `0 17 * * 1-5` | Weekdays at 17:00        |
| `0 0 1 * *`    | First day of every month |

The framework includes cron utilities (`isValidCron()` and `describeCron()`) for validating and rendering cron strings, used internally by the resource and scheduler layers.

## Creating a job {#creating}

### From the Automations UI

Open **Settings › Automations**, then create a **Scheduled** automation. Old
`/agent#jobs` links redirect there.

### By asking the agent

> "Create a scheduled task that summarizes my unread emails every morning at 7."

The agent writes the file for you.

### By hand

Drop a Markdown file in `jobs/` via the framework's resource APIs:

```ts
import { resourcePut } from "@agent-native/core/resources";

await resourcePut(
  ownerEmail,
  "jobs/morning-digest.md",
  `---
schedule: "0 7 * * *"
enabled: true
---
Summarize overnight emails.`,
);
```

## How the scheduler runs {#how-scheduler-runs}

On each tick, the scheduler (the internal `processRecurringJobs()` routine) finds the jobs that are due and runs them. What fires the tick depends on where the app runs:

| Host                                                             | What fires the tick                                                  | Requirements and limits                                                                        |
| ---------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Long-running server (Node.js, Docker, Fly, Render, a VPS)        | An in-process timer every 60 seconds, starting 10 seconds after boot | Runs while the process is up.                                                                  |
| [Netlify](/docs/netlify)                                         | A scheduled function the build emits, every minute                   | Published deploys only, never Deploy Previews or branch deploys. The build needs `A2A_SECRET`. |
| [Vercel](/docs/vercel#vercel-scheduled-work)                     | A Vercel cron job the build adds, every minute by default            | Production deployment only. Needs `CRON_SECRET`. Hobby projects can run it once a day at most. |
| [Cloudflare Workers](/docs/cloudflare#cloudflare-scheduled-work) | A Cron Trigger the build adds to `wrangler.json`, every minute       | Needs `A2A_SECRET` as a Worker secret.                                                         |
| AWS Lambda                                                       | Nothing                                                              | Scheduled automations don't fire.                                                              |
| Local development                                                | Nothing, unless you opt in                                           | See the callout below.                                                                         |

On Netlify, Vercel, and Cloudflare, the same tick also drains the queue of
event-triggered automations, cleans up runs whose function was cut off, and
runs app sweeps such as Mail's snooze and send-later. A serverless deploy
without a trigger gets none of that: event-triggered automations wait in the
queue and interrupted runs stay stuck. Webhook-triggered automations and
**Run now** dispatch on their own, so they still work. When a deploy can't
fire schedules, the **Automations** page says so and names the reason.

<Diagram id="doc-block-14nqnkx" title="One scheduler tick" summary="On each tick the scheduler finds due jobs, then sends them through the same background execution lifecycle as event automations.">

```html
<div class="sched">
  <div class="diagram-box accent">
    scheduler tick &bull; every minute &#8635;
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill">1 &middot; scan</span
    ><small class="diagram-muted"
      >list every enabled <code>jobs/*.md</code> across all owners</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill">2 &middot; due?</span
    ><small class="diagram-muted">compare <code>nextRun</code> to now</small>
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card accent">
    <span class="diagram-pill accent">3 &middot; run</span
    ><small class="diagram-muted"
      >shared runner validates identity and resolves the model, MCP allowlist,
      continuation, delivery, and usage</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card ok">
    <span class="diagram-pill ok">4 &middot; record</span
    ><small class="diagram-muted"
      >write <code>lastRun</code> / <code>lastStatus</code> /
      <code>lastError</code>, recompute <code>nextRun</code></small
    >
  </div>
</div>
```

```css
.sched {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-width: 520px;
}
.sched .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 14px;
}
.sched .diagram-box {
  align-self: flex-start;
}
.sched .diagram-arrow {
  font-size: 18px;
  align-self: center;
}
```

</Diagram>

<Callout id="doc-block-j6yn07" tone="risk">

**A warm instance doesn't run schedules.** Serverless hosts never start the in-process timer, so keep-alive pings don't make jobs fire. Only the platform trigger in the table above does. On AWS Lambda nothing fires them yet, so run scheduled automations on a long-running host (Fly, Render, a VPS) instead.

</Callout>

<Callout id="doc-block-rj2" tone="warning">

**Disabled by default in local dev.** The scheduler's `setInterval` loop never starts when `NODE_ENV` is `development` or `test`, or when the app URL points at `localhost` / `127.0.0.1` / `*.localhost` — which covers most local dev setups. Set `AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS=1` to run the scheduler locally anyway. To turn scheduled jobs off on any host, use `AGENT_NATIVE_DISABLE_RECURRING_JOBS=1`, described next.

</Callout>

<Callout id="doc-block-rj3" tone="warning">

**`AGENT_NATIVE_DISABLE_RECURRING_JOBS` is read in two scopes.** On Netlify, Vercel, and Cloudflare, the **build** environment decides: the platform trigger keeps firing to drain event automations and clean up runs, but a build with the switch on skips scheduled jobs, whatever the deployed environment says. On a long-running server, the **runtime** environment gates the in-process timer. Set it where your host reads it. The build records its decision, so the **Automations** page can say schedules will never run instead of showing a next-run date nothing will honour.

</Callout>

### Running jobs under another `appId` {#additional-job-app-ids}

A scheduler runs a job when the job's `appId` is its app's own `appId`. To run job files labeled with a different `appId` too, list the extra labels in `additionalJobAppIds`:

```ts filename="server/plugins/agent-chat.ts"
export default createAgentChatPlugin({
  appId: "my-app",
  additionalJobAppIds: ["my-app-reports"],
  // ...
});
```

A build that does not list the label skips those files without claiming, running, or writing them, which lets two deployments on one database split their jobs. The option only widens which files this app picks up. The scheduler lease, run history, and health rows stay under `appId`, so deployments that share a database still take turns on one lease. If the app queues **Run now** for those jobs, pass the same list as `additionalJobAppIds` to `queueAutomationRunNow`.

## Debugging a job {#debugging}

- Open the automation's details in **Settings › Automations**, or inspect
  `jobs/<name>.md` in Agent Resources — the frontmatter shows `lastRun`,
  `lastStatus`, `lastError`, and `nextRun`.
- **Test it without waiting:** click **Run now** in **Settings › Automations**, or ask the agent to run it (`manage-automations` with `action=run-now`). The job's prompt runs immediately with its real side effects, and `nextRun` doesn't change. The exception is a job the framework paused: a successful run turns it back on and recomputes `nextRun`. Run now doesn't wait for a scheduler tick, so it also works on localhost without `AGENT_NATIVE_ENABLE_LOCAL_RECURRING_JOBS` and on hosts without a scheduler trigger.
- **Pause it:** flip `enabled: false`. The file stays put, just stops running.

## Agent tool {#agent-tool}

`manage-jobs` is the compatibility tool for existing schedule-only
integrations. New Scheduled and Event automations—including organization
scope, model overrides, and MCP allowlists—use `manage-automations`. The
`action` parameter selects the legacy operation:

| Action   | Parameters                                                                             | Purpose                                                      |
| -------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `create` | `name`, `schedule`, `instructions` (required); `scope`, `runAs`, `model`, `mcpTools`   | Create a new recurring job                                   |
| `list`   | `scope` (`personal`, `shared`, or all)                                                 | List all jobs with status (schedule, enabled, last/next run) |
| `update` | `name` (required); `schedule`, `instructions`, `enabled`, `runAs`, `model`, `mcpTools` | Edit an existing job                                         |
| `delete` | `name` (required)                                                                      | Delete a job — always confirm with the user first            |

`model` is optional on both `create` and `update` — pass a model id to override the channel/app/engine default for that one routine.

**Personal vs shared scope.** Each automation lives in either personal scope
(runs as and is visible only to the creator) or organization scope (visible to
org members, but runs as the creator). Organization administrators can update
or delete shared definitions but cannot change the creator identity. Before
each organization run, the framework verifies that the creator still exists
and remains a member; otherwise it does not execute.

## Different from the scheduling package {#vs-scheduling-package}

Don't confuse recurring jobs with `@agent-native/scheduling`:

- **Recurring jobs (this page)** — cron-scheduled _prompts_ the agent runs in the background. Framework-level. Lives in Agent Resources. Fires on any host with a scheduler (see [How the scheduler runs](#how-scheduler-runs)).
- **`@agent-native/scheduling`** — a reusable domain package for building calendar/booking features (event types, availability windows, bookings). Powers the `calendar` template and custom scheduling surfaces.

Recurring jobs are "how do I make the agent act on its own?" The scheduling package is "how do I build a calendar app?" Different concerns.

## What's next

- [**Automations**](/docs/automations) — the primary Scheduled/Event product model and management workflow
- [**Agent Resources**](/docs/agent-resources) — where jobs live alongside skills, memory, and custom agents
- [**Actions**](/docs/actions-overview) — the tools a job calls
- [**MCP Clients**](/docs/mcp-clients) — connect remote or local MCP tools and OAuth grants
- [**Agent Teams**](/docs/agent-teams) — jobs often spawn sub-agents to do parallel work
- [**Toolkit piece: Toolkit Capability Modules**](/docs/toolkit-capability-packages) — how this differs from the installable Scheduling package
