Scheduling
TF Code can run recurring agent jobs on a cron schedule — for example a daily dependency dig, a weekly report, or a periodic cleanup task. Jobs are managed from the desktop app, the TUI, or by asking the agent (which works from any client attached to a server).
Scheduling is powered by three layers working together:
- OS scheduler units (
launchdon macOS,systemdtimers on Linux) — fire the job at the exact cron time, even when TF Code is not running. - Missed-run recovery — if the machine was off or TF Code wasn't running when a job was due, the job is picked up on the next start (optional, see Missed-run policy).
- Run tracking — every run records timing, status, an optional
sessionID, and logs to disk, so you can audit what ran, when, and whether it succeeded.
Enable
Scheduling is a builtin plugin, disabled by default. Enable it in your config:
{
"builtin": { "scheduler": { "enabled": true } }
}
The server exposes the standard plugin endpoints plus a dedicated /scheduler API:
| Endpoint | Description |
|---|---|
GET /builtin | Plugin status (including scheduler.enabled) |
POST /builtin/scheduler/enable | Enable the scheduler plugin |
POST /builtin/scheduler/disable | Disable the scheduler plugin |
GET /scheduler | List jobs + enabled status |
GET /scheduler/{id} | Job detail + run history |
POST /scheduler | Create a job |
PATCH /scheduler/{id} | Update a job |
DELETE /scheduler/{id} | Delete a job |
POST /scheduler/{id}/run | Run a job immediately |
GET /scheduler/{id}/logs?lines=N | Read the job log tail |
The scheduler only manages jobs while the plugin is enabled. If it's disabled, job-management calls return an error and the panels show an enable prompt.
Cron expressions
Jobs use standard 5-field cron expressions. Example cron values:
| Expression | Meaning |
|---|---|
0 9 * * 1-5 | Weekdays at 09:00 |
30 6 * * * | Every day at 06:30 |
0 0 * * 0 | Sundays at midnight |
*/15 * * * * | Every 15 minutes |
0 3 1 * * | First day of every month at 03:00 |
Fields: minute hour day-of-month month day-of-week.
Agent selection
Each job runs the agent you pick. Set agent to an agent name — it is matched case-insensitively (so BuiLd and build are the same) and the default is build. Unknown or empty names fall back to build.
{
"name": "nightly-report",
"cron": "0 22 * * *",
"agent": "code",
"prompt": "..."
}
When you don't specify an agent, the job always runs with the default build agent.
Missed-run policy
Each job has a missed-run policy for what happens when a scheduled time passes while the machine is off or TF Code isn't running:
| Policy | Behaviour |
|---|---|
catch-up | (default) On the next start, run the most recent missed occurrence once (best-effort recovery). |
exact | Run only at the exact scheduled time — if it's missed while offline, it's skipped. |
Recovery is safe by design:
- It only recovers the single most recent missed occurrence — never a backlog.
- An occurrence that already produced a run is never re-run — including when that run failed. A failing job cannot trigger a continuous re-run loop.
- Recently-due occurrences are left to the OS scheduler for a short grace window, so a job that is simply about to fire while TF Code is online isn't double-run.
- Disabled jobs are never recovered.
Where data and logs live
Job definitions, run history, and logs are stored per project scope in the TF Code state directory:
~/.local/state/tfcode/scheduler/scopes/<scope-id>/
<job-id>.json # job definition
<job-id>.runs.json # run history (status, timing, sessionID, errors)
<job-id>.log # combined job log
runs/<job-id>/<run-id>.out # per-run output
On macOS the path may differ depending on your XDG state directory.
~/.local/stateis the default.
Each run record captures startedAt, finishedAt, status (running | succeeded | failed | crashed | skipped), an optional exitCode, the sessionID of the agent session that executed the run, and any error. Runs left in running after a crash are reconciled to crashed on the next start — they are never rescheduled.
App (Desktop UI)
The web app has a Schedules panel in the session side panel (next to the file / changes tabs).
Create a job
- Open the Schedules panel.
- Click New job.
- Fill in:
- Name — a human-readable label.
- Cron — the schedule, e.g.
0 9 * * 1-5. - Prompt — what the agent should do when the job runs.
- Agent — which agent to run (default
build; matched case-insensitively). - Timeout (s) — how long the run may take before it's killed (
0= no timeout). - Missed-run policy —
Catch uporExact.
- Click Create.
Manage jobs
Each job row shows its next run, last run, last status, and policy, with actions:
| Action | Description |
|---|---|
| Run now | Trigger the job immediately. |
| Pause / Resume | Toggle whether the job is scheduled. |
| Delete | Remove the job and its OS scheduler unit. |
| (expand) | Show the run history for the job. |
If the scheduler plugin is disabled, the panel shows an Enable scheduler button instead.
Server
Any client attached to a running server — including a headless server — schedules jobs by asking the agent, which calls the scheduling tools server-side. There are no user-facing CLI commands; the agent manages jobs on your behalf.
Ask the agent to schedule a job
Schedule a job called "daily-dependency-dig" to run at 9am on weekdays
that updates the dependency report and opens a PR if there are changes.
The agent registers the job with the tools:
schedule_job— create a job (name,cron,prompt,agent,timeout_seconds,missed_run_policy)list_jobs— list jobs for the projectget_job— show one jobupdate_job— change cron, prompt, agent, timeout, enabled, or policydelete_job— remove a jobrun_job— run immediately (fire-and-forget; skips if already running)job_logs— read the last N lines of a job's log
Examples
Schedule a job called "weekly-cleanup" with cron "0 6 * * 1" using the
"code" agent to delete temporary build artifacts and report disk usage.
Set a timeout of 600 seconds.
List my scheduled jobs and show the run history of "daily-dependency-dig".
Run "nightly-backup" right now, then show me its recent logs.
Exact timing with no catch-up
Schedule "publish-daily-cut" with cron "0 17 * * *" and use the
"exact" missed-run policy so it only runs at 17:00 exactly.
TUI
The TUI exposes scheduling through the /schedules dialog and by asking the agent in-session.
/schedules dialog
Type /schedules and press Enter. The dialog lists jobs with their cron, policy, agent, next run, and status. Use the keys shown in the dialog footer:
| Key | Action |
|---|---|
Space | Pause / resume the selected job |
r | Run the selected job now |
d | Delete the selected job |
e | Enable / disable the plugin |
Ask the agent (same as the server)
In the TUI prompt you can schedule, list, update, or delete jobs with natural language, exactly as on a server:
Schedule a job called "morning-standup-brief" to run at 8:30 on weekdays
and summarize the last 24 hours of issues.
Pause "morning-standup-brief" until I say otherwise.
Tips & safety
- A failing job will not loop. Recovery never re-runs an occurrence that produced a run, so failures result in a recorded
failedrun — not a retry storm. - Pause before deleting. Deleting removes the OS unit and job files; pausing just disables the schedule while keeping the job definition.
- Pick
exactfor time-sensitive actions (e.g. a cutover or publish) so an offline period doesn't trigger a late, surprising run. - Review logs before re-running. Use
job_logs(agent) or the/scheduler/{id}/logsendpoint to see why a run failed. - The lock prevents overlap. A job won't start a second run if one is already active.