{"slug": "i-run-my-agency-s-10-ai-agents-from-launchd-no-n8n-0-dependencies", "title": "I run my agency's 10 AI agents from launchd, no n8n, 0 dependencies", "summary": "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.", "body_md": "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.\n\n**The numbers:**\n\n- 10 agents in production at\n[Digiton Dynamics](https://www.digiton.ai) - 0 runtime dependencies, no build step, plain Node\n- 27 unit tests on the rails, CI on Node 18/20/22\n- 1 file per agent\n- 5 rails on every action: kill switch, do-not-contact, allowlist, dedupe, daily cap\n\n``` bash\n$ npm run demo\n\n[INFO] DRAFT ana@oceanview-realty.pt :: allowed, not sent (mode=draft)\n[INFO] BLOCK carlos@montebianco.com :: recipient not on allowlist\n[INFO] BLOCK info@donotcontact.example :: recipient on DNC\n[INFO] done proposed=3 allowed=1 blocked=2 performed=0 killed=false\n```\n\nWhy 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()`\n\nin `lib/rails.js`\n\n. Nothing irreversible happens without clearing it.\n\nSafe by default on a fresh clone: empty allowlist, stub sender, dry-run unless you opt in.\n\n``` php\nproposed action  ->  [ rails ]  ->  sent  or  blocked (with a reason)\ngit clone https://github.com/Botfather90/digiton-agent-fleet\ncd digiton-agent-fleet\n\nnpm test          # run the rails unit tests (zero setup)\nnpm run demo      # watch the rails decide on sample leads (no credentials)\n```\n\nPrefer your own copy: click **Use this template** at the top of the GitHub page\nto start a fresh repo from this one.\n\nThe 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`\n\n, posts a daily digest to a Slack-style webhook channel, a non-email action routed through the same `evaluate()`\n\nso recipient, DNC, allowlist, dedupe, and daily cap all still apply. Run it with `npm run demo:digest`\n\n.\n\nEvery action the agent proposes is checked by `evaluate()`\n\nin `lib/rails.js`\n\n, in this order, hardest stop first:\n\n| Check | Behavior |\n|---|---|\n| Kill switch | `touch state/KILL` freezes every job instantly. Nothing runs until you `rm` it. |\n| Recipient | Must be present, or the action is dropped. |\n| DNC | Do-not-contact list (email or domain). Always wins, even over the allowlist. |\n| Allowlist | An empty allowlist means nothing sends. You opt in addresses or whole domains. |\n| Dedupe | A `dedupeKey` is never actioned twice within the retention window (30 days). History is pruned by age, not kept forever. |\n| Daily cap | Stop after N actions per day. The counter resets at midnight UTC. |\n\n`evaluate()`\n\nis a pure function with no I/O, so it is fast to reason about and fully unit-tested.\n\nThe 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`\n\nand set `\"mode\": \"send\"`\n\n. You have to opt in to every one of those before a single message leaves.\n\nA job is one file in `jobs/`\n\nthat exports two functions:\n\n```\n// jobs/my-agent.js\nmodule.exports = {\n  // Return the actions you want to take. The runner does NOT trust these,\n  // it runs each one through the rails before anything happens.\n  async propose({ config, log }) {\n    return [\n      { to: 'lead@example.com', subject: 'Hello', body: '...', dedupeKey: 'lead-2026-06' },\n    ];\n  },\n\n  // Perform ONE allowed action. This is the only place that actually sends.\n  async send(action, { config, log }) {\n    // your real sender goes here: Gmail, Resend, SES, an API call, whatever\n  },\n};\n```\n\nRun it: `node bin/run-job.js my-agent`\n\n(dry-run) or add `--send`\n\nto dispatch allowed actions.\n\nSet your policy in `fleet.config.json`\n\n(copy `fleet.config.example.json`\n\n):\n\n```\n{\n  \"mode\": \"draft\",\n  \"policy\": {\n    \"dailyCap\": 30,\n    \"allowEmails\": [\"vip@client.com\"],\n    \"allowDomains\": [\"client.com\"],\n    \"dnc\": [\"unsubscribed@x.com\", \"competitor.com\"]\n  }\n}\n```\n\nYour real `fleet.config.json`\n\nis gitignored, so your allowlist never ends up in a commit.\n\nPick your platform. All three run the agent daily at 09:00 by default.\n\n- macOS (launchd):\n`bash schedule/install.sh`\n\ninstalls a launchd job and prints how to run, inspect, and remove it. - Linux (systemd): copy the unit and timer from\n`schedule/systemd/`\n\nand`systemctl enable --now agent-fleet.timer`\n\n. - Anything with cron: paste the line from\n`schedule/crontab.example`\n\ninto`crontab -e`\n\n.\n\n```\nlib/rails.js                 the guardrail (pure evaluate() + ledger persistence)\nlib/log.js                   dated file + stdout logger\nbin/run-job.js               the governed runner\njobs/example-followup.js     a worked example agent (stub sender, safe)\njobs/example-digest.js       a daily digest to a Slack-style webhook channel (a non-email action), through the same rails\ndata/leads.sample.json       sample data so the demo runs with no setup\ndata/digest.sample.json      sample data for the digest example\ntest/rails.test.js           unit tests for the rails and the cadence\ntest/example-digest.test.js  unit tests for the digest job on the same rails\nschedule/                    launchd, cron, and systemd templates + installer\n.github/workflows/test.yml   node:test on push/PR (Node 18/20/22)\n```\n\nMIT. Built and maintained by [Digiton Dynamics](https://www.digiton.ai).", "url": "https://wpnews.pro/news/i-run-my-agency-s-10-ai-agents-from-launchd-no-n8n-0-dependencies", "canonical_source": "https://github.com/Botfather90/digiton-agent-fleet", "published_at": "2026-07-10 22:59:18+00:00", "updated_at": "2026-07-10 23:05:36.050898+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Digiton Dynamics", "n8n", "Docker", "Node.js", "launchd", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/i-run-my-agency-s-10-ai-agents-from-launchd-no-n8n-0-dependencies", "markdown": "https://wpnews.pro/news/i-run-my-agency-s-10-ai-agents-from-launchd-no-n8n-0-dependencies.md", "text": "https://wpnews.pro/news/i-run-my-agency-s-10-ai-agents-from-launchd-no-n8n-0-dependencies.txt", "jsonld": "https://wpnews.pro/news/i-run-my-agency-s-10-ai-agents-from-launchd-no-n8n-0-dependencies.jsonld"}}