WIRL is a lightweight DSL, runner, and set of apps for automating routines with deterministic, inspectable workflow graphs. Workflows support retries, branching, cycles, and Human‑in‑the‑Loop (HITL) as first‑class concepts. LLMs are pluggable (local or hosted), while control‑flow remains fixed and auditable.
- You’re exploring ways to automate personal or work routines.
- You like Python/LangGraph, but scaling beyond 1–2 ad‑hoc workflows became messy (state, reuse, approvals, retries).
- You tried n8n/low‑code, but the visual model is heavy, hard for AI to modify, and versioning/custom code gets awkward.
- You want reproducible research/AI pipelines with checkpoints and retries, without adopting a full MLOps stack.
- You prefer workflow descriptions that are human‑readable and AI‑generatable, separated cleanly from infra and the runner.
- Python 3.11+
- uv (recommended) or pip
- Optional: an OpenAI‑compatible endpoint (e.g., Ollama locally or a hosted API)
git clone https://github.com/madmag77/wirl
cd wirl
make workflows-setup
# creates .venv, installs local packages, and installs per-workflow requirementsUse the provided Make target:
make run-workflow \
WORKFLOW=<workflow_name> \
FUNCS=workflow_definitions.<workflow_name>.<workflow_name> \
PARAMS="key1=value1 key2=value2"Example — Paper rename:
make run-workflow \
WORKFLOW=paper_rename_workflow \
FUNCS=workflow_definitions.paper_rename_workflow.paper_rename_workflow \
PARAMS='drafts_folder_path="/Users/username/Library/Mobile Documents/com~apple~CloudDocs/papers_unsorted" processed_folder_path="/Users/username/Library/Mobile Documents/com~apple~CloudDocs/papers_sorted"'If you already have Ollama installed or plan to use a commercial LLM API, you can run most workflows immediately.
To run regularly, see run history, and perform approvals, bring up the backend and frontend:
- Provision Postgres (Docker, a local install, or a free Supabase instance). For Mac (latest versoin only) users there is a script at
scripts/container-start-postgres.shthat sets up the container with postgres for you. - Export the Postgres connection environment variables required by the backend (e.g., DATABASE_URL, for local setup it's
postgresql://postgres:postgres@localhost:5432/workflows). - Set environment variable
WORKFLOW_DEFINITIONS_PATHto the absolute path to workflow definitions. Like this:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/workflows
WORKFLOW_DEFINITIONS_PATH=/Users/username/wirl/workflow_definitions- Start the apps:
make run_wirl_apps # or: overmind start
# Frontend: http://localhost:3000
# Backend: http://localhost:8000With the apps running you can start, continue, retry, and provide HITL inputs from the UI. The runs list now supports paging, and each run exposes View Run Details to inspect every step’s inputs and outputs.
To run the apps automatically after reboot:
scripts/macos/mac-install-launchctl.shWhen running via launchctl, logs are stored in ~/.local/log/:
# View live output logs
tail -f ~/.local/log/wirl-workflows-overmind.out
# View live error logs
tail -f ~/.local/log/wirl-workflows-overmind.err
# View both logs simultaneously
tail -f ~/.local/log/wirl-workflows-overmind.{out,err}
# Check service status
launchctl list | grep com.local.wirl.overmind
# Connect to individual processes via Overmind
overmind connect backend -s ~/.overmind.sock
overmind connect workers -s ~/.overmind.sock
# Detach with: Ctrl-b, then d
# Restart the service
launchctl unload ~/Library/LaunchAgents/com.local.wirl.overmind.plist
launchctl load ~/Library/LaunchAgents/com.local.wirl.overmind.plistNote: The install script (scripts/macos/mac-install-launchctl.sh) now automatically:
- Loads
DATABASE_URLandWORKFLOW_DEFINITIONS_PATHfrom.envfile and adds them to the plist - Cleans up stale socket files before starting the service
- Provides detailed debugging output after installation
If the WIRL services aren't running properly via launchctl, use these debugging commands:
Check if services are running:
# List all WIRL-related services
launchctl list | grep -i wirl
# Output format: PID EXIT_CODE SERVICE_NAME
# - PID: Process ID if running, "-" if not running
# - EXIT_CODE: 0 = success, 1+ = error
# Example output:
# - 1 com.local.wirl.overmind <- Service crashed (exit code 1)
# 2309 0 com.apple.container... <- Service running (PID 2309)Get detailed service status:
# Detailed info about the service
launchctl print gui/$(id -u)/com.local.wirl.overmind
# Shows: state, runs count, last exit code, log paths, environment variablesCheck recent logs:
# View last 50 lines of error log (most useful for debugging)
tail -50 ~/.local/log/wirl-workflows-overmind.err
# View last 50 lines of output log
tail -50 ~/.local/log/wirl-workflows-overmind.out
# Follow logs in real-time
tail -f ~/.local/log/wirl-workflows-overmind.{out,err}Common issues and solutions:
-
Stale socket file (service exits with code 1):
- Symptom: Error log shows "it looks like Overmind is already running"
- Solution: The plist template now auto-cleans stale socket files. If you installed before this fix, manually remove:
rm -f ~/path/to/wirl/.overmind.sock # Service will auto-restart after cleanup
- Note: If you modified the plist template before this fix, reinstall with:
scripts/macos/mac-install-launchctl.sh
-
Missing environment variables:
- Symptom: Backend or workers crash immediately after starting
- Cause: Required environment variables (
DATABASE_URL,WORKFLOW_DEFINITIONS_PATH) are not set - Solution:
- Create a
.envfile in the repo root with required variables:
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/workflows WORKFLOW_DEFINITIONS_PATH=/absolute/path/to/wirl/workflow_definitions
- Reinstall the service (it will automatically pick up the
.envfile):
scripts/macos/mac-install-launchctl.sh
- Verify environment variables were added:
launchctl print gui/$(id -u)/com.local.wirl.overmind | grep -A 20 "environment ="
- Create a
-
Service not loaded at all:
- Symptom:
launchctl list | grep wirlreturns nothing - Solution: Reinstall the service:
scripts/macos/mac-install-launchctl.sh
- Symptom:
-
Service continuously restarting (high runs count):
- Symptom:
launchctl printshows "runs = 49+" with exit code 1 - Solution: Check error logs to identify the root cause:
tail -100 ~/.local/log/wirl-workflows-overmind.err - Symptom:
-
All services exit immediately after starting:
- Symptom: Output log shows services starting then immediately "Interrupting..." and "Exited with code 0"
- Cause: Procfile contains a short-lived process that exits immediately (e.g., a command that just checks status and exits)
- Solution: All processes in the procfile must be long-running. If you need initialization, combine it with a long-running command:
# Bad: exits immediately container: sh -c 'container system start' # Good: initialization followed by long-running command postgres: sh -c 'container system start; container start postgres; container logs -f postgres'
- Why: Overmind interprets any process exit as a signal to shut down all services. Keep processes alive with
-fflags,tail -f,sleep infinity, or similar.
Check running services (once overmind is up):
# Check status of all services managed by overmind
overmind status -s /path/to/repo/.overmind.sock
# Example output:
# PROCESS PID STATUS
# postgres 11639 running
# backend 11640 running
# workers 11641 running
# frontend 11642 running
# Connect to a specific service (to see its live output)
overmind connect workers -s /path/to/repo/.overmind.sock
# Press Ctrl-b then d to detachManual service control:
# Unload (stop) the service
launchctl unload ~/Library/LaunchAgents/com.local.wirl.overmind.plist
# Load (start) the service
launchctl load ~/Library/LaunchAgents/com.local.wirl.overmind.plist
# Or use the bootstrap/bootout commands (newer macOS)
launchctl bootout gui/$(id -u)/com.local.wirl.overmind
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.local.wirl.overmind.plistVerify overmind works manually:
cd ~/path/to/wirl
overmind start # Should start all services without errors
# Press Ctrl-C to stop when testingIf overmind works manually but fails via launchctl, the issue is likely environment-related (PATH, env vars, working directory).
WIRL now ships with a built-in cron-style scheduler so you can run workflows on a cadence without deploying extra services:
- You can find Scheduled Triggers panel on the main page above the workflow runs history.
- Click Schedule Workflow to define a name, pick a workflow template, provide JSON inputs, and enter a cron expression + timezone.
- Toggle the trigger on. The backend persists it to Postgres and shows the next run time.
The FastAPI backend hosts a lightweight scheduler that polls the workflow_triggers table and enqueues runs whenever a trigger is due. The worker service picks up the queued run just like any manual execution, so no additional infrastructure is required. You can pause/resume triggers from the UI; invalid templates or cron expressions automatically disable the trigger and surface the error message inline.
If you prefer external schedulers, you can still wire GitHub Actions (see infra/github_actions_document_sort.yaml) or any other job runner against the public API.
- Runs list with paging.

- View Run Details for a single run: step timeline, status, retry count, and captured inputs/outputs per node.

- HITL page to approve or edit at workflow checkpoints, then resume execution.

There are four example workflows (three are used daily):
- Paper rename You save papers to papers_unsorted with unreadable names (e.g., 2411.06037v3.pdf). This workflow reads new PDFs, uses a vision‑capable model via an OpenAI‑compatible endpoint (e.g., Ollama) to extract title/author/year from the first pages, and saves the file with a proper name in papers_sorted. You only consult the sorted folder.
- News digest You provide sources. Each morning the workflow checks for new posts since the last run, summarizes them with a local LLM, and emails a digest with links. You read the digest and open only what matters.
- Photo → notes (HITL)
Many people snap photos/screenshots instead of writing notes (announcements, tracking numbers, invoices). The workflow collects yesterday’s images from a designated folder (Automator/Shortcuts can assist on macOS to fill this folder with your last photos), extracts useful info with a vision model, and emails you a report plus a link to the local HITL page. You open a link and write simple instructions (“keep only the tracking number with a description”, “make a note from this invoice”), and the workflow applies them and writes the final note to Obsidian or any
mdbased note-taking system. - AutoRater evaluation (new) A reproducible evaluation pipeline inspired by Google’s “autorater” idea: sample items (e.g., from HotpotQA), run model‑as‑judge prompts to assess whether context is sufficient prior to answering, aggregate metrics, and export a report. Extend with multiple rater models and a reduce step to compare systems.
- DSL: A compact, readable language to declare nodes, edges, typed IO, when branches, cycle{... guard ...} loops, and hitl{...} checkpoints.
- Runner: Pregel‑style execution with checkpointing, retries, resume, and HITL pause/resume built in.
- Apps: Backend + workers + frontend for approvals and observability
packages/
wirl-lang/ # DSL grammar + parser
wirl-pregel-runner/ # Pregel-style runner, checkpoints, HITL
workflow_definitions/
paper_rename_workflow/
...
extensions/vscode/ # syntax highlighting for VS Code/Cursor
apps/ # frontend + workers + backend (HITL + runs UI)
scripts/ # setup/launch helpers
infra/ # macOS launchctl, CI examples, etc.
- Separate the plan from the code.
- The graph lives in a .wirl file. This is the "plan": nodes, edges, branches, cycles, guards, and HITL.
- The logic lives in pure Python functions that match each node's call target.
- LLM‑friendly flow.
- Use Cursor/Windsurf to generate the .wirl graph from a natural‑language description. There is a very detailed
Agents.mdfile to help LLMs do it with good quality. - Generate the Python module with node functions (pure, testable).
- Review the .wirl diff to verify control‑flow instantly.
- Use Cursor/Windsurf to generate the .wirl graph from a natural‑language description. There is a very detailed
- Deterministic control‑flow. Models can be stochastic; the graph is not. Guards and cycle limits are explicit and reviewable.
When authoring .wirl workflows, follow these fundamental rules:
-
Input and output parameters are mandatory: Every workflow must declare
inputandoutputparameters. Workflows without both will not execute properly. -
First node must depend on an input parameter: The first node to run must have a dependency on at least one of the workflow's input parameters. Without this dependency, the workflow will not start execution.
-
Cycle node inputs are restricted: Inside cycles, nodes can only use:
- Inputs from neighboring nodes within the cycle
- Inputs of the cycle itself
Inputs from outside the cycle are not directly accessible and must be proxied through cycle input parameters to be available inside the cycle.
-
Dotted notation inside cycles: Inside a cycle, all input values must use dotted notation, even when referencing the cycle's own inputs. For example, if you're inside a cycle named
ProcessItems, reference cycle inputs asProcessItems.cycleInputrather than justcycleInput.
- Keep functions pure and idempotent where possible.
- Use simple, typed inputs/outputs.
- Avoid hidden globals and side effects besides intentional outputs.
# workflow_definitions/your_flow/steps.py
from dataclasses import dataclass
from typing import List, Tuple
@dataclass
class PhotoInfo:
path: str
def get_photos(folder: str, days_back: int) -> List[PhotoInfo]:
...
def extract_note(image_b64: str) -> str:
...
def append_and_check(notes: List[str], note: str, remaining: List[PhotoInfo]) -> Tuple[List[str], bool]:
# returns (updated_notes, is_done)
...python -m wirl_pregel_runner.pregel_runner \
workflow_definitions/<name>/<name>.wirl \
--functions workflow_definitions.<name>.<module> \
--param key=value --param other=valOr via Make:
make run-workflow \
WORKFLOW=<name> \
FUNCS=workflow_definitions.<name>.<module> \
PARAMS="key1=value1 key2=value2"- Runs list (paged).
- Run details with inputs/outputs per node, timestamps, durations, retry counts.
- Checkpoints at every node allow precise resume and retry.
- Data model: persisted in Postgres via
workflow_triggers(seeapps/backend/backend/models.py). - API: CRUD endpoints under
/workflow-triggersinapps/backend/backend/main.pyexpose trigger management to the UI. - Scheduler:
apps/backend/backend/scheduler.pyruns inside the FastAPI process, polling for due triggers and enqueueing runs. - Duplicate protection: the scheduler locks each due trigger row with
SELECT ... FOR UPDATE SKIP LOCKED, refreshesnext_run_at, and aligns cron evaluation to the prior fire time so the same trigger cannot be enqueued twice within the same minute even if multiple pollers overlap. - UI: React components in
apps/frontend/src/components/WorkflowTriggersTable.jsxallow creating, editing, pausing, and deleting triggers alongside manual runs. - Workers: no change required; they keep polling the
workflow_runsqueue and execute runs spawned by triggers.
- Declare a checkpoint with hitl { correlation: "...", timeout: "..." }. Those parameters are not being used and preserved for future implementation.
- The runner pauses and persists state.
- Approvers act in the UI; the run resumes from the checkpoint with the decision recorded.
- LLM endpoints: Any OpenAI‑compatible base URL/key (Ollama locally or hosted APIs).
- Database: Postgres for run metadata and checkpoints (set DATABASE_URL or the backend’s required env vars).
- Secrets: Provide via env vars; swap for your secret manager as needed.
- Scheduling: use the built-in cron runner (FastAPI
backend/scheduler.py, UI inapps/frontend/src/components/WorkflowTriggersTable.jsx) or wire any external system such as LaunchAgents/systemd, containers, or GitHub Actions (see infra/).
- Create workflow_definitions/<your_workflow>/.
- Add <your_workflow>.wirl (graph) and <your_workflow>.py (node functions).
- (Optional) Add requirements.txt scoped to this workflow.
- Run:
make run-workflow \
WORKFLOW=<your_workflow> \
FUNCS=workflow_definitions.<your_workflow>.<your_workflow> \
PARAMS="..."- Inspect in the UI, use View Run Details, iterate.
- Keep node IO simple, serializable, and explicit.
- Reuse functions across workflows; treat .wirl files as the configuration surface.
- Favor guarded loops over unbounded iteration.
- Capture enough per‑node IO to debug, but redact sensitive fields when needed.
- Types in workflows are not being used now, they are only for you and LLM to understand the workflow better
- Cycles are only sequential for now, there is a plan to introduce parallel execution at scale
- Parameters in HITL block (correlation and timeout) are not being used
- There are no first class concept "retry" implemented yet (ideally you should be able to mark any node as retryable with parameters)
MIT.
