npm install @petriflow/rules
read-before-send

Don't message blindly

The agent must read messages before it can send. Each read earns one send. The sequencing rule cycles — after each send, the agent must read again.

require discord.readMessages before discord.sendMessage limit discord.sendMessage to 5 per session
// agent tries to send without reading first
Agent calls discord({ action: "sendMessage", channel: "#dev-general", content: "Hello!" })
BLOCKED readMessages required first
Agent calls discord({ action: "readMessages", channel: "#dev-general" })
OK earns one send token
Agent calls discord({ action: "sendMessage", channel: "#dev-general", content: "Hello!" })
ALLOWED send token consumed. Budget: 4/5
Agent tries to send another message immediately
BLOCKED must readMessages again (rule cycles)

1:1 read-to-send ratio + session rate limit. The agent can't spam a channel — every send requires a fresh read, and messages are capped at 5 per session. Full tutorial →

test-before-deploy

Don't ship broken code

Lint gates test. Test gates deploy. Deploy requires a human and is capped at 2 per session — no prompt injection can skip the pipeline or exhaust your deploys.

require lint before test require test before deploy require human-approval before deploy limit deploy to 2 per session
// agent tries to deploy
Agent calls deploy({ environment: "production" })
BLOCKED lint hasn't run, test hasn't run, human hasn't approved
Agent calls lint()
OK linter passes, test unlocked
Agent calls test()
OK tests pass, deploy one gate away
PetriFlow calls confirm: "Allow 'deploy'?"
Human approves → manual gate opens
Agent calls deploy({ environment: "production" })
ALLOWED pipeline satisfied. Budget: 1/2 remaining
After 2 deploys, a third attempt
BLOCKED rate limit reached — budget exhausted

Deploy is a manual gate with a budget. Even if the agent is prompt-injected, deploys are structurally blocked until lint, test, and human approval are all satisfied. And the session limit prevents runaway deploys. Full tutorial →

research-before-share

Don't share fabrications

Each fetch earns one share token. Each share consumes one. The agent can't broadcast "findings" it never actually fetched.

require web-fetch before share-result
// agent tries to share a research summary
Agent calls slack.sendMessage({ text: "Research summary: React 20 is out..." })
BLOCKED no fetch has been done
Agent calls webFetch({ url: "https://react.dev/blog" })
OK earns one share token
Agent calls slack.sendMessage({ text: "React blog says..." })
ALLOWED share token consumed
Agent calls slack.sendMessage({ text: "Also, Vue 5 is..." })
BLOCKED no token left, must fetch again

1:1 fetch-to-share ratio. The agent can't hallucinate research results and share them — every share must be backed by an actual web fetch.

backup-before-delete

Don't destroy without a safety net

Only mapped bash commands can reach the shell. Everything else is blocked. The backup must actually succeed before delete is unlocked (deferred check).

# allow only these bash commands: map bash.command cp as backup map bash.command rm as delete # block everything else: block bash # enforce ordering: require backup before delete
// agent tries to clean up a data directory
Agent calls bash({ command: "rm -rf data/" })
BLOCKED rm maps to delete, no backup yet
Agent calls bash({ command: "unlink data/temp.log" })
BLOCKED unlink is unmapped, caught by block bash
Agent calls bash({ command: "cp -r data/ /tmp/data-backup" })
OK cp maps to backup, deferred until confirmed
Agent calls bash({ command: "rm -rf data/" })
ALLOWED backup succeeded, delete unlocked

Allowlist, not deny-list. unlink, shred, find -delete — anything unmapped is blocked. Only cp and rm can reach the shell, and only in the right order. Full tutorial →

whatsapp messaging

Don't message the wrong person

Three rules compose to create a safe messaging flow: lookup before send, human approval, and rate limiting.

# look up the recipient first require whatsapp.lookup before whatsapp.send # human confirms recipient + message require human-approval before whatsapp.send # one send per lookup cycle limit whatsapp.send to 1 per whatsapp.lookup
// agent tries to send a WhatsApp message
Agent calls whatsapp({ action: "send", to: "+1415...", message: "Hey!" })
BLOCKED no recipient lookup yet
Agent calls whatsapp({ action: "lookup", query: "Sarah" })
OK resolves to +14155551212
Agent calls whatsapp({ action: "send" })
BLOCKED human hasn't approved yet
PetriFlow prompts: "Send 'Hey!' to Sarah (+14155551212)?"
Human approves → manual gate opens
Agent calls whatsapp({ action: "send", to: "+14155551212", message: "Hey!" })
ALLOWED lookup done, human approved, token consumed
Agent calls whatsapp({ action: "send", message: "One more thing" })
BLOCKED budget consumed, must look up again

Three rules compose into one safe flow. The agent can't guess phone numbers, can't send without human confirmation, and can't batch-fire messages after a single lookup.