---
title: Routines
description: "Routines run an app op on its own, on a timer, with no chat open."
sidebar:
  label: Routines
  order: 13
---

## What you can do

**Run an op on a timer.** A Routine invokes one of an app's declared ops on its own, hourly, daily, or weekly, without a chat open and without the owner doing anything. Use it for a poll, a refresh, a daily check, or any op that should just keep running.

**Up to three per app, twenty-five per account.** Each app can carry up to 3 Routines; each account, up to 25 across every app. Both caps are fixed today, not configurable.

**Stops itself after repeated failures.** A Routine that fails 5 times in a row disables itself and emails the owner. Re-enabling it resets the failure count and starts the clock over.

## Technical

A Routine invokes its target op with empty input (`{}`) on the schedule you set. It runs as the app itself, the same as any other server-side call — it does not count as the owner opening the app.

### Copy this prompt for your agent

```text
Set up a Routine for my Charming app. Read
https://charm.ing/docs/capabilities/routines.md first. Work out
which op should run on its own and how often, then create the
Routine and tell me when it'll first run.
```

### How an agent performs this job

Use [`create_routine`](/docs/technical-reference/mcp/tools/create_routine) to schedule a declared op, [`list_routines`](/docs/technical-reference/mcp/tools/list_routines) to see the caller's Routines and their run state, [`update_routine`](/docs/technical-reference/mcp/tools/update_routine) to change the interval or enable/disable one, and [`delete_routine`](/docs/technical-reference/mcp/tools/delete_routine) to stop and remove one. The matching HTTP routes are `POST /api/v1/apps/{appId}/routines` (create), `GET /api/v1/routines` (list), `PATCH /api/v1/routines/{routineId}` (update), and `DELETE /api/v1/routines/{routineId}` (delete) — session-gated, and behaviorally identical to their MCP twins.

A worked example:

```text
create_routine({ app_id: "<uuid>", op: "refreshFeed", interval: "hourly" })
→ Created Routine routine_<uuid>: runs `refreshFeed` hourly on <app>.

list_routines()
→ routine_<uuid> — `refreshFeed` on app <uuid>, hourly
```

If `refreshFeed` then fails on 5 ticks in a row, the Routine disables itself (`enabled: false`, `disabled_reason: "auto"`), the owner gets an email with a ready-to-paste fix prompt, and `next_run_at` reads `null` until it's re-enabled with `update_routine({ routine_id, enabled: true })`.

### The contract

`interval` is a closed enum: `hourly`, `daily`, or `weekly` — never raw cron syntax. The target op must accept empty input; an op with any required input field cannot be scheduled. `create_routine` and its HTTP twin reject a second Routine on the same `(app, op)` pair.

`list_routines` and every other Routines response return a `routine` object with `id`, `app_id`, `op`, `interval`, and seven run-state fields:

| Field                  | Meaning                                                     |
| ---------------------- | ----------------------------------------------------------- |
| `enabled`              | `false` once the Routine is owner-disabled or auto-disabled |
| `disabled_reason`      | `"owner"`, `"auto"`, or `null` when enabled                 |
| `next_run_at`          | Next scheduled run (ISO 8601), or `null` while disabled     |
| `last_run_at`          | Last tick's timestamp, or `null` before the first run       |
| `last_outcome`         | Last tick's result, or `null` before the first run          |
| `last_error`           | `{ kind, message }` for the last failure, or `null`         |
| `consecutive_failures` | Resets to 0 on any success or on re-enable                  |

Four error kinds are specific to Routines:

| Error kind               | Fires when                                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------------------------- |
| `routine_limit_exceeded` | The per-app (3) or per-user (25) cap is already met                                                           |
| `op_requires_input`      | The target op declares a required input field, so it can't run with empty input                               |
| `duplicate_routine`      | A Routine already exists for this `(app, op)` pair                                                            |
| `invalid_interval`       | Internal guard for the interval-to-cron mapping; not reachable through normal `hourly`/`daily`/`weekly` input |

The generated [MCP reference](/docs/technical-reference/mcp) and [REST API reference](/docs/reference) define the full request and response shapes.

### Limits, access, and deletion

- **Caps.** 3 Routines per app, 25 per account, hourly is the shortest interval available. See [Limits](limits).
- **Access.** Creating, updating, and deleting a Routine requires the same `app:write` access as editing the app itself. See [Privacy and sharing](privacy-and-sharing).
- **Deletion.** Deleting the app or the account cascades to its Routines; there's no separate cleanup step.
- **Not a Routine tick.** An unattended tick never counts as the owner using the app — it doesn't affect the app's activity history the way an owner's own call would.

## Related

- [Privacy and sharing](privacy-and-sharing): a Routine runs with the same `app:write` gate as editing the app
- [Limits](limits): the 3-per-app / 25-per-user caps and hourly floor
- [How Charming works](../concepts/how-charming-works)
- [Docs home](..)
- [llms-full.txt](https://charm.ing/docs/llms-full.txt)

Found a bug or need a feature? [Tell us](/docs/capabilities/feedback) with `submit_feedback` or `POST /app/{id}/feedback`. Your feedback shapes what we build next.
