Automations¶
Automations run recurring or one-time work: briefings, maintenance scripts, periodic code reviews, or anything else on a timer. Their config definition is the source of truth; ScheduledTask is only Pynchy's internal runtime projection.
Three execution shapes share the scheduler:
- Agent tasks run the owning workspace's selected agent core, on the host or in a container according to its profile.
- Deterministic workspace tasks run a host command without an LLM and post its output through an owned workspace thread.
- Host tasks run infrastructure commands without a conversational workspace.
Persistent Automation Memory¶
When Obsidian learning is enabled, every scheduled task owns a durable directory at wiki/systems/pynchy/automation-memory/<task-id>/ in the configured vault. Pynchy exposes that directory as PYNCHY_AUTOMATION_MEMORY_DIR to agent tasks, pre-run gates, deterministic workspace commands, and host commands. Container agents see /home/agent/automation-memory; host processes receive an absolute host path.
Memory defaults on. Set memory = false in an automation's [job] table to omit the directory, mount, and environment variable for that automation. Disabling memory doesn't delete an existing task directory.
This memory belongs to the task ID, not its thread or provider session, so both continue and reset_before_run preserve it. Pausing or removing a task leaves its directory intact. Renaming a config-backed job creates a new task ID and therefore a new memory directory.
All execution shapes use the canonical Obsidian directory directly. Pynchy doesn't create or synchronize a runtime-owned mirror.
Agent Tasks¶
Agent tasks run an agent on schedule. The agent gets a prompt and uses its owning workspace's normal tools, skills, repo, admin status, and execution mode as if a user had sent the message. A config-backed agent job names its policy owner explicitly with workspace.
Pynchy derives one human-readable thread for each config-backed job. It names the thread <workspace> | <display_name>, falling back to the config job name. Every run finds or creates that thread, including reopening an archived one; the persisted binding records the current JID while the logical workspace continues to own policy. A semantic workspace can be physically placed below a category while retaining its own profile. For example, a fam job may create fam | afternoon check-in under #relationships, but the thread runs and remains registered with only the fam profile. Different jobs under the same workspace use different threads and can run concurrently.
Temporal buffers one overlapping occurrence for a config-backed job. The next run waits for the current one, then runs in the same task thread; Pynchy never creates a numbered spillover thread for that job. This requires a channel with child-thread support. Pynchy records an error instead of moving the run to another target when the root channel cannot create threads.
Linear planning and execution tasks bind to the issue's routed conversation, so every phase uses the issue thread's existing runtime. Pynchy refuses to run a task whose destination cannot be bound.
Every scheduled task uses one of two session policies:
continueresumes the thread's current provider session.reset_before_runclears the thread before each occurrence, posts🗑️, and starts a new durable provider session.
Configured agent jobs set reset_before_run = true by default. The reset is visible even on the first occurrence. Temporal retries reuse the session created for that occurrence and don't post another reset. Set the field to false when successive occurrences should build on the same context.
A scheduled turn and ordinary messages share the thread's queue. A normal message interrupts scheduled work after the current tool result, runs next in the same session, and leaves the scheduled checkpoint available for Temporal to resume. At most one worker owns the thread.
An agent completes a run by returning its final result. The worker process can then stop, but the durable session remains resumable. Repo-backed agents can publish with sync_worktree_to_main, which opens or updates a pull request for committed changes. They resolve any error it returns and attach the PR to the current Linear issue when one exists. Scheduled prompts don't need sentinel commits.
During a scheduled run, the agent tries to resolve ordinary snags, bugs, and tool failures itself. After the primary objective, the default post-work reflection prompt asks it to review unresolved bugs, failures, and workflow papercuts; search existing Linear and papercut records; and file only missing reports. It doesn't report problems that it fixes during the run. The prompt fragment lives at data/defaults/prompts/executors/post-work-reflection.md and is injected into scheduled agent work after the automation objective.
For a periodic review that turns evidence into approval-gated work proposals, see Schedule proactive proposals.
Documentation gardener¶
The disabled pynchy-docs-gardener example reviews a rotating slice of Pynchy documentation once a week. It validates and reports concrete problems, but creates only Agent Proposed Linear todos; each proposed repair remains subject to normal approval and review. Copy the directory into your personalization repository, confirm the pynchy workspace has the needed repository and Linear access, then set enabled = true.
Daily Triage Memo¶
A daily triage memo is a config-backed periodic agent that posts a short status memo to its owned thread. Keep it read-only by prompt, reset its context before each occurrence, and use a cheaper workspace model override:
# data/personalization/pynchy.toml
[profiles.admin]
is_admin = true
[workspaces.admin]
profiles = ["admin"]
model = "chatgpt/gpt-5.3-codex-spark"
# data/personalization/automations/daily-triage/config.toml
schema_version = 1
[job]
enabled = true
schedule = "0 8 * * *"
workspace = "admin"
reset_before_run = true
prompt = """
Produce the daily Pynchy triage memo.
Review recent scheduled task health, failed runs, Temporal scheduler status,
stale PR/branch/CI signals if available, and recent Pynchy/operator notes.
Keep the run read-only except for writing a dated memo/report note if useful.
Do not edit config, cron jobs, branches, PRs, or external systems.
Send a concise memo to this Pynchy channel every run:
- Top 3 findings or "no urgent findings".
- Any failing or paused scheduled work.
- Suggested next actions with concrete repo paths, URLs, or commands when useful.
- Links/paths to any full report you wrote.
"""
The example model name must exist in the active LiteLLM config. For Codex workspaces backed by LiteLLM's ChatGPT subscription provider, keep the chatgpt/... prefix.
One-time agent jobs use at instead of schedule:
# data/personalization/automations/cancel-youtube-premium/config.toml
schema_version = 1
[job]
enabled = true
at = "2026-07-08T18:30:00-07:00"
workspace = "admin"
prompt = """
Open a browser, log into YouTube, and cancel the YouTube Premium subscription.
"""
Use interval_minutes for config-backed interval jobs. An agent job can also run a host-side gate before starting its agent:
# data/personalization/automations/marketplace-poller/config.toml
schema_version = 1
[job]
workspace = "marketplace-inbox-poller"
interval_minutes = 30
display_name = "marketplace inbox poller"
prompt = "Review the gate output and act on actionable messages."
pre_run_command = "./scripts/marketplace_gate.py"
pre_run_timeout_seconds = 300
If the final non-empty line of successful gate output is JSON containing "wakeAgent": false, Pynchy records a skipped run without starting an agent. The job's durable thread binding still exists. Otherwise, stdout and stderr become bounded pre-run context for the agent.
Deterministic Workspace Tasks¶
Set agent = false for a script that does not need an LLM but still belongs to a conversational workspace:
# data/personalization/automations/scheduler-watchdog/config.toml
schema_version = 1
[job]
workspace = "cron"
schedule = "0 23 * * *"
display_name = "scheduler health watchdog"
agent = false
command = "./scripts/scheduler_watchdog.py"
timeout_seconds = 300
The command runs on the host. Non-empty output goes to the derived thread under the workspace's physical Discord root, and Pynchy registers that thread with the logical owner's profile for future replies. Successful output ending in {"wakeAgent": false} skips delivery but retains the job's thread binding. Pynchy creates or repairs active and paused task bindings at startup, before their first run. Discord forum workspaces tag these posts as automation.
Plugin-Sourced Jobs¶
Plugins can implement pynchy_job_specs to load jobs from another durable registry. Contributions use the same JobConfig fields and enter the same database and Temporal reconciliation paths as file-backed automations. Personalized config wins on name collisions. A plugin should store logical workspace owners, never chat JIDs or generated thread folder names.
Host jobs use the reserved workspace name host:
# data/personalization/automations/backup-runtime-dbs/config.toml
schema_version = 1
[job]
enabled = true
schedule = "0 3 * * *"
workspace = "host"
command = "./scripts/backup_runtime_dbs.sh"
timeout_seconds = 600
quiet_on_success = true
Temporal Scheduler¶
Pynchy reconciles scheduled work into Temporal. Recurring agent tasks, database host jobs, and config-file host cron jobs become Temporal Schedules. One-time agent tasks and one-time host jobs become delayed Temporal workflows.
Temporal fires the workflows. Each workflow runs an activity in the Pynchy host process, so agent containers, IPC streaming, shell execution, task logs, and worktree isolation stay on the existing host runner path.
Host commands are not retried within the same Temporal occurrence. A command may have changed external state before failing or losing its worker, so its next scheduled occurrence is the retry boundary.
Long-running agent activities heartbeat while they run. If the host restarts, Pynchy uses the durable interrupted-turn checkpoint to continue an unfinished scheduled agent in its existing conversation instead of starting the task prompt again.
[scheduler]
temporal_address = "localhost:7233"
temporal_namespace = "default"
temporal_task_queue = "pynchy-scheduler"
reconcile_schedules = true
git_sync_interval_seconds = 300
channel_reconciliation_interval_seconds = 300
auto_deploy = false
When auto_deploy stays false (the default), Pynchy detects a newer repository revision without changing the local checkout. It posts an update prompt to the configured admin workspace. Use the channel's Fetch and upgrade action to fetch the revision and deploy it. Channels without interactive controls direct the operator to run uv run pynchy deploy on the host. Set auto_deploy = true to pull and deploy eligible source changes automatically.
Pynchy requires a reachable Temporal service when the scheduler starts. It does not fall back to local due-work execution. The local scheduler loop only reconciles desired state from config and SQLite into Temporal; it does not decide that a task is due or run shell commands itself.
Set reconcile_schedules = false only for a shadow migration instance. Its Temporal worker remains available for explicit test workflows, but Pynchy does not create recurring schedules or delayed workflows from configuration and SQLite. Enable reconciliation on exactly one authoritative instance.
The /status endpoint includes a temporal section:
| Field | Meaning |
|---|---|
address | Configured Temporal server address |
namespace | Configured Temporal namespace |
task_queue | Task queue used by the Pynchy scheduler worker |
cluster_healthy | Result of the Temporal WorkflowService health check (true, false, or null if unreachable) |
worker_running | Whether this Pynchy process has an active Temporal worker |
last_workflow_id | Most recent scheduled-work workflow started or handled by this process |
last_task_id | Scheduled task or host job ID for the most recent workflow event |
last_result | started, already_started, completed, skipped, or error |
last_error | Last scheduler dispatch or activity error, if any |
Single-host macOS service¶
For a personal macOS deployment, run a local Temporal service bound to loopback with a persisted SQLite database:
brew install temporal
mkdir -p ~/Library/Logs/pynchy data
cp launchd/com.pynchy.temporal.plist ~/Library/LaunchAgents/com.pynchy.temporal.plist
Before loading the plist, replace $HOME with your absolute home directory and $PYNCHY_PROJECT_ROOT with the absolute path to this checkout. launchd does not expand shell variables inside plist string values.
plutil -lint ~/Library/LaunchAgents/com.pynchy.temporal.plist
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.pynchy.temporal.plist
launchctl kickstart "gui/$(id -u)/com.pynchy.temporal"
Useful checks:
launchctl print "gui/$(id -u)/com.pynchy.temporal"
temporal operator cluster health --address 127.0.0.1:7233
lsof -nP -iTCP:7233 -sTCP:LISTEN
tail -n 100 ~/Library/Logs/pynchy/temporal.err.log
The local service stores its durable state at data/temporal.db. Back up this file with the rest of data/; losing it drops Temporal workflow history, schedule state, delayed starts, and idempotency state for scheduled work. This single-host setup is suitable for a personal Mac deployment. Use Temporal Cloud or a normal self-hosted Temporal cluster when the scheduler needs HA or multi-host durability.
To back up runtime databases with SQLite-safe snapshots, run:
The script backs up messages.db, neonize.db, and temporal.db into data/backups by default and prunes backups older than 30 days. Set PYNCHY_BACKUP_KEEP_COUNT to a positive integer to also cap the number of retained generations; 0 leaves the count uncapped. The launchd template keeps the newest seven generations. It briefly unloads the com.pynchy.temporal LaunchAgent while snapshotting temporal.db, then loads it again. This prevents the online SQLite backup from blocking Temporal writes and leaves other Pynchy components running. Set PYNCHY_TEMPORAL_LABEL and PYNCHY_TEMPORAL_PLIST when the deployment uses different launchd identifiers.
For host-loss protection, set both PYNCHY_BACKUP_REMOTE_HOST and PYNCHY_BACKUP_REMOTE_DIR. The script creates SQLite snapshots in PYNCHY_BACKUP_STAGING_DIR, transfers them with rsync, verifies SHA256SUMS on the remote host, and only then renames the hidden partial directory to its final timestamp. Set PYNCHY_BACKUP_SSH_KEY when the scheduled job needs a dedicated noninteractive key. Remote hosts must provide bash, rsync, and sha256sum:
PYNCHY_BACKUP_REMOTE_HOST=backup@example-nas \
PYNCHY_BACKUP_REMOTE_DIR=/srv/backups/pynchy-runtime-dbs \
PYNCHY_BACKUP_SSH_KEY="$HOME/.ssh/pynchy_backup_ed25519" \
scripts/backup_runtime_dbs.sh
To run a remote backup daily on macOS:
Before loading the plist, replace $HOME and $PYNCHY_PROJECT_ROOT with absolute paths, and replace $PYNCHY_BACKUP_REMOTE_HOST and $PYNCHY_BACKUP_REMOTE_DIR with the remote SSH host and absolute destination. Ensure the host key is already trusted and the configured key works with BatchMode=yes.
plutil -lint ~/Library/LaunchAgents/com.pynchy.backup.plist
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.pynchy.backup.plist
launchctl kickstart "gui/$(id -u)/com.pynchy.backup"
Host Tasks¶
Host tasks run shell commands on the host — no LLM, no container. Use declared automation files for maintenance scripts, backups, git operations, or anything that doesn't need an agent.
Automation file¶
Static host jobs belong in data/personalization/automations/. They are useful for always-on maintenance jobs that are part of the deployment.
# data/personalization/automations/backup-db/config.toml
schema_version = 1
[job]
enabled = true
workspace = "host"
schedule = "0 3 * * *" # daily at 3am
command = "./scripts/backup.sh"
timeout_seconds = 600 # default: 600
quiet_on_success = true # suppress clean-run output logging
Config host jobs use workspace = "host" and currently support cron expressions. Pynchy reconciles enabled config host jobs into Temporal Schedules, and Temporal triggers host-process activities for each run. They appear in list_automations because it reads config definitions, not database rows.
Pynchy validates and applies automation-file changes without restarting. An add, update, disable, removal, workspace reassignment, or referenced prompt change updates the configured task rows and Temporal schedules together. An invalid edit or failed reconciliation leaves the previous runtime snapshot active and retries during the next configuration poll.
MCP Tools¶
list_automations reports visible config-backed definitions, including host scripts, deterministic workspace commands, and prompt-only agent automations. Use it, then get_automation, before creating a similar automation. All changes write the automation config and reconcile its runtime projection.
| Tool | Purpose |
|---|---|
list_automations | Show visible config-backed automation definitions |
get_automation | Read one automation, including its prompt or command |
create_automation | Create one config-backed automation |
update_automation | Update one automation definition |
pause_automation | Disable an automation without deleting it |
resume_automation | Enable a paused automation |
delete_automation | Delete an automation definition and its automation-owned files |
send_message | Send a message to the group (agent tasks only) |
list_todos | List pending todo items (or all items with include_done: true) |
complete_todo | Mark a todo item as done by ID |
Only admin workspaces can create, update, pause, resume, or delete automations. Non-admin workspaces can read definitions owned by their workspace. pause_automation and resume_automation change enabled in the config; runtime reconciliation keeps schedule and memory behavior intact.
Schedule Types¶
Config-backed agent and deterministic workspace jobs support these schedule types:
| Type | Value Format | Example |
|---|---|---|
cron | Cron expression | 0 9 * * 1 (Mondays at 9am) |
interval | Milliseconds | 3600000 (every hour) |
once | ISO timestamp | 2024-12-25T09:00:00Z |
Config-backed agent and deterministic workspace jobs use schedule for cron, interval_minutes for intervals, or at for one-time execution. Config-file host jobs only support cron.
cron and interval entries run as Temporal Schedules. once entries run as delayed Temporal workflows. Temporal owns the wake-up; Pynchy activities own the actual agent or host-shell execution.