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

# Schedules

> Start an instance or a job later, once at a set time or on a repeating schedule, with a maximum runtime on every run.

export const RuncrateStyles = () => {
  if (typeof document !== 'undefined' && !document.getElementById('runcrate-overrides')) {
    const s = document.createElement('style');
    s.id = 'runcrate-overrides';
    s.textContent = `
      /* Match Runcrate's rounding scale (--radius: 0.75rem) */
      .rounded-sm { border-radius: 0.5rem !important; }   /* 8px */
      .rounded-md { border-radius: 0.625rem !important; } /* 10px */
      .rounded-lg { border-radius: 0.75rem !important; }  /* 12px */
      .rounded-l-sm { border-top-left-radius: 0.5rem !important; border-bottom-left-radius: 0.5rem !important; }
      .rounded-r-sm { border-top-right-radius: 0.5rem !important; border-bottom-right-radius: 0.5rem !important; }
      .rounded-l-md { border-top-left-radius: 0.625rem !important; border-bottom-left-radius: 0.625rem !important; }
      .rounded-r-md { border-top-right-radius: 0.625rem !important; border-bottom-right-radius: 0.625rem !important; }
      .rounded-l-lg { border-top-left-radius: 0.75rem !important; border-bottom-left-radius: 0.75rem !important; }
      .rounded-r-lg { border-top-right-radius: 0.75rem !important; border-bottom-right-radius: 0.75rem !important; }

      /* Cards: never pure white in light mode */
      .card { background-color: #fcfcfc !important; border-radius: 0.75rem !important; }
      html.dark .card { background-color: #141414 !important; }

      /* Docs hero box */
      .rc-hero { background-color: #fcfcfc; border: 1px solid #e0e0e0; }
      html.dark .rc-hero { background-color: #141414; border-color: #242424; }
      html.dark .rc-hero h1 { color: #f5f5f5; }

      /* Runcrate scrollbar — thin, transparent track, hide-until-hover thumb */
      ::-webkit-scrollbar { width: 6px; height: 6px; background-color: transparent; }
      ::-webkit-scrollbar-track { background-color: transparent; }
      ::-webkit-scrollbar-thumb { background-color: rgba(155, 155, 155, 0.5); border-radius: 10px; transition: opacity 0.3s ease; opacity: 0; }
      ::-webkit-scrollbar-thumb:hover { background-color: rgba(155, 155, 155, 0.7); }
      *:hover::-webkit-scrollbar-thumb,
      *:focus::-webkit-scrollbar-thumb,
      *:active::-webkit-scrollbar-thumb { opacity: 1; }
      * { scrollbar-width: thin; scrollbar-color: rgba(155, 155, 155, 0.5) transparent; }
    `;
    document.head.appendChild(s);
  }
  return null;
};

<RuncrateStyles />

A schedule starts an [instance](/docs/instances/overview) or a [job](/docs/jobs/overview) for you later: once at a time you pick, or again and again on a repeating schedule in your timezone. Your nightly training run starts at 02:00 London time without anyone awake to launch it.

Every schedule has a maximum runtime, so nothing it starts runs unbounded. Runcrate checks schedules every 2 minutes, so a run starts within about 2 minutes of its time.

## How a schedule runs

<Steps>
  <Step title="Create">
    You describe what to start (the same settings as launching an instance or submitting a job), when to start it, and a maximum runtime. The settings are checked now, so a schedule that is accepted can launch.
  </Step>

  <Step title="Wait">
    The schedule shows its next run. You can pause, resume, change or delete it at any time.
  </Step>

  <Step title="Launch">
    At its time, Runcrate starts the instance or job as the person who set the schedule up (who created it, or last changed its spec or timing, or resumed it), through the same kind of sign-in or API key they used, in the environment it was created in. It is billed like any instance or job you start yourself.
  </Step>

  <Step title="Record">
    The schedule keeps the result of its last run (`launched`, `failed` or a skip), the id of what it started, and how many runs it has made.
  </Step>
</Steps>

## Create a schedule

<Tabs>
  <Tab title="Dashboard">
    In the launch sheet, turn on **Start later**:

    1. Pick **Once** and a date and time, or **Repeat** and a preset: **Daily**, **Weekdays**, **Weekly** or **Custom** (a cron expression).
    2. Check the timezone. It starts as your browser's.
    3. Set a maximum runtime. It is required while **Start later** is on.
    4. Click **Schedule**. Nothing launches now.

    Your schedules are on the **Schedules** tab of the **Instances** page. Each row shows when it runs, its next run and its last result, with actions to pause, resume, run now and delete.
  </Tab>

  <Tab title="Ask your agent">
    Connect the [Runcrate MCP server](/docs/mcp/setup) to Claude Code, Codex, Cursor or any MCP client, then ask in plain words:

    ```text theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
    Every night at 2am London time, run python train.py on an H100 with my volume training-data, for at most 2 hours.
    ```

    ```text theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
    Start an A100 instance called demo-box tomorrow at 9am New York time and stop it after 8 hours.
    ```

    ```text theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
    Pause my nightly-train schedule until next week, and tell me how its last run went.
    ```

    What the agent does with the first prompt:

    1. `list_volumes` to find the id of `training-data`.
    2. `create_schedule` with `kind: "job"`, `cron: "0 2 * * *"`, `timezone: "Europe/London"` and a `spec` of `{ "command": "python train.py", "gpu_type": "H100", "max_runtime_minutes": 120, "storage_id": "<volume-id>" }`. It gets the schedule back with its `next_run_at`.
    3. Tells you when the first run is.

    For the second prompt it calls `create_schedule` with `kind: "instance"`, a `run_at` with the New York offset, and a `spec` with `name`, `gpu_type` and `max_runtime_minutes: 480`. For the third it finds the schedule with `list_schedules`, reads `last_status` and `last_error`, and pauses it with `update_schedule` and `enabled: false`. `run_schedule_now` starts a run straight away. See [MCP tools](/docs/mcp/tools#schedules) for every parameter.
  </Tab>

  <Tab title="See the code">
    The CLI takes a time with `--at` or a cron expression with `--cron`. A command after `--` schedules a job; without one it schedules an instance:

    ```bash theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
    # A job every night at 02:00 London time
    runcrate schedule create --cron '0 2 * * *' --tz Europe/London --name nightly-train \
      --gpu H100 --volume <volume-id> --max-runtime 2h -- python train.py

    # An instance once, tomorrow at 09:00 New York time, stopped after 8 hours
    runcrate schedule create --at '2026-10-21 09:00' --tz America/New_York \
      --name demo-box --gpu A100 --max-runtime 8h
    ```

    The same job schedule over the API:

    <CodeGroup>
      ```bash Create theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
      curl -X POST https://www.runcrate.ai/api/v1/schedules \
        -H "Authorization: Bearer $RUNCRATE_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "kind": "job",
          "name": "nightly-train",
          "cron": "0 2 * * *",
          "timezone": "Europe/London",
          "spec": {
            "command": "python train.py",
            "gpu_type": "H100",
            "max_runtime_minutes": 120,
            "storage_id": "<volume-id>",
            "env": { "HF_TOKEN": "hf_..." }
          }
        }'
      ```

      ```bash Once theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
      curl -X POST https://www.runcrate.ai/api/v1/schedules \
        -H "Authorization: Bearer $RUNCRATE_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "kind": "instance",
          "run_at": "2026-10-21T09:00:00-04:00",
          "timezone": "America/New_York",
          "spec": { "name": "demo-box", "gpu_type": "A100", "max_runtime_minutes": 480 }
        }'
      ```

      ```bash Pause theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
      curl -X PATCH https://www.runcrate.ai/api/v1/schedules/$SCHEDULE_ID \
        -H "Authorization: Bearer $RUNCRATE_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{ "enabled": false }'
      ```

      ```bash Run now theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
      curl -X POST https://www.runcrate.ai/api/v1/schedules/$SCHEDULE_ID/run-now \
        -H "Authorization: Bearer $RUNCRATE_API_KEY"
      ```
    </CodeGroup>

    With the SDKs:

    <CodeGroup>
      ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
      import Runcrate from '@runcrate/sdk';

      const rc = new Runcrate({ apiKey: process.env.RUNCRATE_API_KEY });

      const schedule = await rc.schedules.create({
        kind: 'job',
        name: 'nightly-train',
        cron: '0 2 * * *',
        timezone: 'Europe/London',
        spec: { command: 'python train.py', gpuType: 'H100', maxRuntimeMinutes: 120 },
      });
      console.log(schedule.nextRunAt);

      await rc.schedules.update(schedule.id, { enabled: false }); // pause
      const { job } = await rc.schedules.runNow(schedule.id);      // run once now
      ```

      ```python Python theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
      import os
      from runcrate import Runcrate

      client = Runcrate(api_key=os.environ["RUNCRATE_API_KEY"])

      schedule = client.schedules.create(
          kind="job",
          name="nightly-train",
          cron="0 2 * * *",
          timezone="Europe/London",
          spec={"command": "python train.py", "gpu_type": "H100", "max_runtime_minutes": 120},
      )
      print(schedule.next_run_at)

      client.schedules.update(schedule.id, enabled=False)  # pause
      run = client.schedules.run_now(schedule.id)           # run once now
      ```
    </CodeGroup>

    The full request and response shapes are in the [API reference](/docs/api-reference).
  </Tab>
</Tabs>

## Once or repeating

A schedule is one or the other: send `run_at` to run once, or `cron` to repeat.

* **Once.** Pick a real date and time in the future, at most 365 days ahead (`2027-02-30` is refused). After it runs, the schedule stays in the list, paused, with no next run and its result.
* **Repeating.** A standard 5-field cron expression, `minute hour day-of-month month day-of-week`, read in the schedule's timezone. Runs must be at least 15 minutes apart. If runs were missed while Runcrate could not start them, the schedule runs once and moves on to its next time; it does not catch up on every missed run.

The dashboard presets are cron expressions you can also send yourself:

| Preset | Example | Cron |
| - | - | - |
| Daily | Every day at 02:00 | `0 2 * * *` |
| Weekdays | Monday to Friday at 09:00 | `0 9 * * 1-5` |
| Weekly | Every Sunday at 02:00 | `0 2 * * 0` |
| Custom | Every 6 hours, on the hour | `0 */6 * * *` |

Each field accepts `*`, a value, a range `a-b`, a step `*/n` or `a-b/n`, and lists such as `1,15`. Months take `JAN` to `DEC`, days of the week take `SUN` to `SAT` or `0` to `7` (both `0` and `7` are Sunday). When you restrict both the day of the month and the day of the week, a day that matches either one runs, as in standard cron.

## Timezones and daylight saving

A schedule's `timezone` is an IANA name such as `Europe/London` or `America/New_York`, and `UTC` if you leave it out. A repeating schedule keeps its wall-clock time across daylight saving changes: `0 2 * * *` in London runs at 02:00 London time in winter and in summer.

Twice a year a local time is missing or happens twice:

* **A time that does not exist that day** (clocks go forward) runs at the same offset after the gap: a 02:30 run happens at 03:30.
* **A time that happens twice** (clocks go back) runs once, the first time.

On a one-off, `run_at` is an exact instant and the timezone is used only to show it.

## Maximum runtime

Every schedule needs a maximum runtime (`spec.max_runtime_minutes`), so a run nobody is watching stops on its own.

* **Jobs:** 1 minute to 24 hours, counted from when the command starts, like any [job](/docs/jobs/overview).
* **Instances:** 5 minutes to 7 days. The instance stops when it reaches it, as with auto stop on an instance you launch yourself.

## When a run is skipped

A run that is skipped does not launch anything and is not retried. The schedule moves on to its next run.

* **Overlap.** By default (`overlap: "skip"`), a repeating schedule skips a run while the instance or job from its previous run is still live, so a slow run never stacks up a second machine. Set `overlap: "allow"` to start every run regardless. The last result is `skipped_overlap`.
* **Budget.** When the environment's monthly budget is spent, the run is skipped. The last result is `skipped_budget`.

## Who a run launches as

Anyone with access to the environment sees and manages every schedule in it, like instances. Each run launches as the schedule's `run_as` person, through the credential they used (`run_as_credential`: `user` for a dashboard sign-in, `api_key` for an API key, `agent` for an MCP or CLI agent sign-in), and is billed to the workspace like anything they launch.

`run_as` starts as the person who created the schedule (`created_by`, which never changes). It moves to whoever later changes its spec, its timing (`run_at`, `cron` or `timezone`) or its `overlap`, or resumes it. Pausing or renaming does not move it.

Before each run, Runcrate checks that the credential still holds:

* If the `run_as` person has left the workspace, the schedule is paused with the last result `no_access`.
* If the API key behind it has been revoked or no longer belongs to this environment, or the agent's sign-in has ended, the schedule is paused with the last result `credential_revoked`.

So revoking an API key or ending an agent's sign-in stops the schedules it set up. To take one over, resume it: from then on it runs as you, through the credential you resumed it with.

When many schedules are due at the same time, each workspace gets its turn, so one busy workspace does not hold up another.

## What everyone in the environment can read

Everything in `spec` except environment variable values, including `launch_script` and `startup_commands`, is returned to everyone with access to the environment. `env` (jobs) and `env_vars` (instances) come back only as their names in `env_keys`, but their values are kept with the schedule and copied to each machine it starts. For tokens and passwords, use your environment's secrets, set in the dashboard: every instance and job gets them when it starts, stored encrypted, so they never need to be in a schedule. Never put them in a script or command.

## Notifications

When a scheduled launch fails, or a schedule is paused because its `run_as` person lost access or its credential was revoked, Runcrate emails the workspace's owners and admins and sends a `schedule_failed` event to the workspace's webhooks (set up under **Workspace settings**), like the budget and auto stop alerts.

The webhook data carries `schedule_id`, `schedule_name`, `environment_id`, `kind`, `status` (`failed`, `failing`, `no_access` or `credential_revoked`), `error` and `scheduled_for`.

A repeating schedule whose scheduled runs fail 5 times in a row is paused (`status: failing`, one notification instead of the fifth `failed`), so a schedule that can't start stops alerting you every time. Run now failures don't count, and a launch, a change or a resume starts the count again.

Skipped runs do not notify: a budget skip follows the budget alert you already received, and an overlap skip means the previous run is still going.

## Pause, resume and run now

* **Pause** stops future runs (`enabled: false`). Nothing already running is stopped.
* **Resume** (`enabled: true`) runs a repeating schedule at its next occurrence from now. A one-off resumes only if its time is still in the future; otherwise send a new `run_at`.
* **Run now** launches the instance or job straight away as you, whether the schedule is paused or not. It does not move the next run, and its result becomes the last result (`run_count` counts runs that launched).
* **Delete** removes the schedule. Instances and jobs it already started keep running.

Changing `spec` replaces it whole. Environment variable values are never returned (you get their names in `env_keys`), so send them again when you change the spec.

## Last result

| `last_status` | Meaning |
| - | - |
| `launched` | The instance or job started. `last_instance_id` or `last_job_id` names it. |
| `failed` | The launch failed. `last_error` says why, and a notification went out. After 5 failed scheduled runs in a row the schedule is also paused, and `last_error` starts with `Paused after 5 failed runs in a row.` |
| `skipped_overlap` | The previous run was still live and `overlap` is `skip`. |
| `skipped_budget` | The environment's monthly budget was spent. |
| `no_access` | The `run_as` person no longer has access to the workspace, so the schedule was paused. Resume it to run it as you. |
| `credential_revoked` | The API key behind it was revoked or moved out of this environment, or the agent's sign-in ended, so the schedule was paused. Resume it to run it as you. |

## Limits

| Limit | Value |
| - | - |
| Start time | Within about 2 minutes of the scheduled time (schedules are checked every 2 minutes) |
| Schedules | 50 per environment, 200 per workspace |
| Repeating | Runs at least 15 minutes apart |
| Once | A real date and time in the future, at most 365 days ahead |
| Maximum runtime | Required. Jobs 1 minute to 24 hours, instances 5 minutes to 7 days |
| Name | 1 to 63 characters |

## Errors

| Code | Status | When |
| - | - | - |
| `validation_error` | 422 | A field is missing or out of range, both or neither of `run_at` and `cron` were sent, the time is not a real date, is in the past or is too far ahead, or the cron runs more often than every 15 minutes. |
| `not_found` | 404 | The schedule does not exist in this workspace and environment. |
| `conflict` | 409 | The environment already has 50 schedules or the workspace 200, or you resumed a one-off whose time has passed without a new `run_at`. |
| `BUDGET_REACHED` | 402 | Run now: the environment's monthly budget is spent. |
| `insufficient_credits` | 402 | Run now: your balance cannot cover the launch. |
| `service_unavailable` | 503 | Schedules are briefly unavailable. Retry shortly. |

## Workspaces and environments

Schedules belong to a workspace and an environment, like instances and jobs. An API key or agent session sees only the schedules in its own environment. Otherwise, pick one with the `X-Environment` header or the `environment` query parameter (MCP: the `env` parameter).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.