Skip to main content
A workflow with a trigger.schedule does not fire on its own - something has to be watching the clock. That something is px0d, a deliberately dumb scheduler: it polls workflows/, evaluates cron expressions in machine local time, spawns px0 workflows run <id> --quiet for anything due, and runs one housekeeping pass a day. Dumb is the goal here. There is no queue to inspect, no job state to reconcile, and nothing to recover from a corrupt scheduler state - the workflow files are the schedule.

1. Give a workflow a schedule

Standard five-field cron. If you described the timing in your original sentence - “every friday at 5pm” - the builder already wrote this for you. Invalid expressions are rejected when the workflow is validated, not silently ignored at fire time. A schedule that never fires is one of the worst failure modes an automation tool can have, so px0 refuses to save one.
A scheduled workflow must write to a file (output: {target: file, path: ...}) or to your inbox (output: {target: inbox}). Nothing is watching stdout when the run fires, so px0 treats stdout-plus-schedule as a validation error rather than losing the output.

Which clock the schedule is read against

By default the daemon evaluates cron in the machine’s local time. That is right until the machine moves: a laptop carried two timezones over silently shifts every “9am” report by two hours, and the same happens twice a year without moving at all.
A workflow’s own trigger.timezone wins over the store default. A zone this machine does not know fails validation rather than falling back silently, because a silent fallback looks like it worked and fires at the wrong hour. px0 daemon status reports each next fire on the same clock the daemon will use.

Or watch instead of scheduling

A workflow does not have to fire on the clock at all. trigger.watch polls a read-only tool and runs the workflow when an item it has not seen before turns up:
The first poll only records a baseline, so pointing a watch at a busy source does not immediately fire on everything already there. Composio’s own event triggers need a public endpoint to deliver to, which a laptop does not have - this is what a local-first tool does instead. What was new is piped to the run on stdin, so the workflow acts on the items that triggered it rather than going back to look and getting a different answer. min_items waits until that many have turned up; anything held back still counts on the next poll.
See Workflow files.

2. Install the daemon

px0 picks the mechanism your OS actually wants: Force the cron fallback with --fallback-cron if you would rather not use the native one.
status tells you whether the process is genuinely alive - it signals the recorded pid rather than trusting a pidfile - plus the last fire per workflow and each scheduled workflow’s next upcoming fire. That last part is the quickest way to sanity-check a cron expression you are unsure about. To run it in the foreground instead, which is what you want while debugging a schedule:

3. What it does while it runs

The loop polls every 30 seconds. On each tick it fires anything whose cron schedule is due, and polls any trigger.watch whose interval has elapsed, firing on anything new. A workflow with enabled: false is skipped by both, and by the cron fallback’s generated crontab block. On the first tick of a new calendar day it also runs the nightly pass: Every one of those steps is fault-isolated. A broken index or an unreachable playlist is recorded in the report, and the rest of housekeeping still runs - one bad URL should not cost you a week of version checkpoints.

4. Missed fires

If your machine was asleep or the daemon was down, the fires it missed are not silently dropped. On startup and on every tick, px0 compares each workflow’s schedule against its last recorded fire, runs what it owes you, and marks anything discovered well after it was due as late in the run record. So a laptop that was closed on Friday afternoon still gets its Friday digest when it wakes up - and the record tells you it arrived late, so you are not left wondering why a 5pm digest is timestamped Saturday morning.

5. Watch what it is doing

Timestamps in the log are UTC; cron schedules are evaluated in machine local time. That is worth remembering when a fire looks an hour off from what you expected. The log lives at logs.path. There is no rotation yet, so if it ever gets unwieldy, truncate it - nothing reads it back. For one run rather than the daemon as a whole:
--follow on a run that has already finished prints what is there and returns immediately rather than hanging, so it is safe to use on any run id without checking first.

6. Scheduled runs are just runs

Everything a scheduled fire does is recorded exactly like a manual run, with trigger: schedule on the record:
That is the loop for debugging a schedule that misbehaved: find the run, read what it actually did, fix the workflow file, rerun it by hand to confirm, and let the next fire pick up the corrected file.

Running without the daemon

You do not have to install it. Without the daemon, trigger.schedule and trigger.watch are documentation, every run is one you started, and the nightly housekeeping simply does not happen - which mostly means hand edits are checkpointed on the next run instead of overnight, and you reindex the brain yourself with px0 brain reindex. If your machine already has a scheduler you trust, pointing it at px0 workflows run <id> --quiet is a perfectly reasonable substitute.

Uninstalling the scheduler

Removes whatever install put in place - the launchd plist or the systemd unit, unloaded first - and stops the daemon if it is running. Workflows keep their schedules; nothing fires on its own again until px0 daemon install runs again. On the cron fallback, it says that px0’s entries are still in your crontab rather than editing it - that file is yours. install.sh --uninstall also removes the scheduler unit, but it removes px0 along with it.

Retries and failure notifications

A workflow can be told how many times to retry itself and how to tell you when it still fails, in its own retry and on_failure frontmatter, or store-wide with px0 config set:
A workflow’s own on_failure block wins over these, so the noisy hourly job can stay quiet while the nightly report shouts. See Workflow files.

When px0 gives up

An unattended workflow that fails the same way runs.disable_after_failures times in a row (5 by default) is parked, and you are told through the same channel failures use. A dead connector otherwise means an hourly failure and an hourly notification for the rest of the week, with nothing learning that nothing has changed.
It requires the same cause each time - a workflow failing three different ways is one to look at, not one stuck. A manual run never trips it: you are there, reading the error. The park is a versioned change like any other, so px0 changes revert undoes it, and px0 workflows enable is the ordinary way back. px0 workflows health reports a parked workflow as a problem in its own right. Set px0 config set runs.disable_after_failures 0 to let a workflow keep trying forever.