I run my agency's 10 AI agents from launchd, no n8n, 0 dependencies Digiton Dynamics runs 10 production AI agents using a plain Node.js framework with zero dependencies, replacing complex orchestration tools like n8n. Each agent is a single file triggered by launchd, and every irreversible action passes through a unit-tested guardrail system that enforces kill switches, allowlists, do-not-contact lists, deduplication, and daily caps to prevent errors. The open-source template is designed to be safe by default, requiring explicit opt-in before any messages can be sent. Ten scheduled AI agents run my agency's operations: lead research, PR outreach, reply triage, pipeline sync, inbox monitoring, daily briefings. No n8n, no framework, no queue, no Docker. Each agent is one Node file on a launchd timer, and every irreversible action passes a unit-tested guardrail before it happens. This repo is that harness, extracted clean as a template. The numbers: - 10 agents in production at Digiton Dynamics https://www.digiton.ai - 0 runtime dependencies, no build step, plain Node - 27 unit tests on the rails, CI on Node 18/20/22 - 1 file per agent - 5 rails on every action: kill switch, do-not-contact, allowlist, dedupe, daily cap bash $ npm run demo INFO DRAFT ana@oceanview-realty.pt :: allowed, not sent mode=draft INFO BLOCK carlos@montebianco.com :: recipient not on allowlist INFO BLOCK info@donotcontact.example :: recipient on DNC INFO done proposed=3 allowed=1 blocked=2 performed=0 killed=false Why the rails exist: we once ran an unguarded email bot on a timer in production. No allowlist, no daily cap, no off switch - one bad prompt or one bad data row away from mailing the wrong list at 3am. We shut it down and rebuilt every scheduled agent behind a single pure function, evaluate in lib/rails.js . Nothing irreversible happens without clearing it. Safe by default on a fresh clone: empty allowlist, stub sender, dry-run unless you opt in. php proposed action - rails - sent or blocked with a reason git clone https://github.com/Botfather90/digiton-agent-fleet cd digiton-agent-fleet npm test run the rails unit tests zero setup npm run demo watch the rails decide on sample leads no credentials Prefer your own copy: click Use this template at the top of the GitHub page to start a fresh repo from this one. The demo runs the example follow-up agent in dry-run against three sample leads and shows the rails at work: one allowed, one blocked for not being on the allowlist, one blocked by the do-not-contact list. A second worked example, jobs/example-digest.js , posts a daily digest to a Slack-style webhook channel, a non-email action routed through the same evaluate so recipient, DNC, allowlist, dedupe, and daily cap all still apply. Run it with npm run demo:digest . Every action the agent proposes is checked by evaluate in lib/rails.js , in this order, hardest stop first: | Check | Behavior | |---|---| | Kill switch | touch state/KILL freezes every job instantly. Nothing runs until you rm it. | | Recipient | Must be present, or the action is dropped. | | DNC | Do-not-contact list email or domain . Always wins, even over the allowlist. | | Allowlist | An empty allowlist means nothing sends. You opt in addresses or whole domains. | | Dedupe | A dedupeKey is never actioned twice within the retention window 30 days . History is pruned by age, not kept forever. | | Daily cap | Stop after N actions per day. The counter resets at midnight UTC. | evaluate is a pure function with no I/O, so it is fast to reason about and fully unit-tested. The template cannot email anyone on a fresh clone. The example sender is a deliberate stub, the default allowlist is empty, and the runner is dry-run unless you pass --send and set "mode": "send" . You have to opt in to every one of those before a single message leaves. A job is one file in jobs/ that exports two functions: // jobs/my-agent.js module.exports = { // Return the actions you want to take. The runner does NOT trust these, // it runs each one through the rails before anything happens. async propose { config, log } { return { to: 'lead@example.com', subject: 'Hello', body: '...', dedupeKey: 'lead-2026-06' }, ; }, // Perform ONE allowed action. This is the only place that actually sends. async send action, { config, log } { // your real sender goes here: Gmail, Resend, SES, an API call, whatever }, }; Run it: node bin/run-job.js my-agent dry-run or add --send to dispatch allowed actions. Set your policy in fleet.config.json copy fleet.config.example.json : { "mode": "draft", "policy": { "dailyCap": 30, "allowEmails": "vip@client.com" , "allowDomains": "client.com" , "dnc": "unsubscribed@x.com", "competitor.com" } } Your real fleet.config.json is gitignored, so your allowlist never ends up in a commit. Pick your platform. All three run the agent daily at 09:00 by default. - macOS launchd : bash schedule/install.sh installs a launchd job and prints how to run, inspect, and remove it. - Linux systemd : copy the unit and timer from schedule/systemd/ and systemctl enable --now agent-fleet.timer . - Anything with cron: paste the line from schedule/crontab.example into crontab -e . lib/rails.js the guardrail pure evaluate + ledger persistence lib/log.js dated file + stdout logger bin/run-job.js the governed runner jobs/example-followup.js a worked example agent stub sender, safe jobs/example-digest.js a daily digest to a Slack-style webhook channel a non-email action , through the same rails data/leads.sample.json sample data so the demo runs with no setup data/digest.sample.json sample data for the digest example test/rails.test.js unit tests for the rails and the cadence test/example-digest.test.js unit tests for the digest job on the same rails schedule/ launchd, cron, and systemd templates + installer .github/workflows/test.yml node:test on push/PR Node 18/20/22 MIT. Built and maintained by Digiton Dynamics https://www.digiton.ai .