Heartbeats & Routines
When you first hire a few agents, it's tempting to give each one a timer — "wake up every few minutes and see if there's anything to do." It feels proactive. In practice, it's the fastest way to end up with a sidebar full of paused agents, surprise token bills, and a dashboard you have to fight with just to keep things quiet.
Paperclip is designed around a different default: agents stay dormant until real work arrives. This guide explains the two mechanisms that drive an agent — heartbeats on an interval and routines — and when to reach for each one. It also walks through the Routines UI, from the list view down to the detail page where you configure triggers, variables, and run history.
The two ways an agent starts running
An agent can be woken up in four ways:
| Source | What triggers it |
|---|---|
| Timer | A heartbeat interval ticks (e.g. every 5 minutes, whether or not there's work) |
| Assignment | A task is assigned to the agent, or a new comment arrives on one of its open tasks |
| On demand | You click "Wake now" in the UI, or another agent hands it work |
| Automation | A routine fires on its schedule and hands the agent an issue to execute |
The first — timer — is the only one that wakes an agent without anything having actually happened. The other three are event-driven: the agent runs because there's something to do, not in case there's something to do.
Why timer heartbeats are opt-in
A new agent ships with timer heartbeats turned off by default. That's deliberate, and it's the setting you should usually keep.
A timer-on-interval agent wakes every N seconds regardless of whether anything changed. Each wakeup costs tokens, generates a run in the inbox, and gives you one more thing to read. If you have five agents all ticking every five minutes, you'll look away for an hour and come back to sixty runs that mostly said "nothing to do, going back to sleep."
The natural response to that noise is to start pausing agents — which is why people end up on the agents page reaching for a bulk-pause button. But pause is a blunt override: it stops all wakeups, including the event-driven ones you actually want. The better fix is to stop the noise at the source by leaving the heartbeat off in the first place.
Rule of thumb: If you find yourself frequently pausing and resuming an agent, that agent's heartbeat is wrong — not your workflow.
Heartbeat settings on an agent
Open any agent and find the Run Policy section. You'll see:
- Heartbeat on interval — the master switch. Off by default. Turn it on only when the agent needs to poll something that doesn't have its own event trigger (see "When a timer is the right answer" below).
- Run heartbeat every N sec — only visible when the interval is on. If you need a timer at all, prefer minutes or hours, not seconds.
Under Advanced Run Policy you'll also find:
- Wake on demand — leave this on. It's what lets assignments, comments, and routines reach the agent.
- Cooldown (sec) — minimum gap between runs, useful for bursty work.
- Max concurrent runs — how many tasks the agent can work in parallel.
The combination you want for most agents: Heartbeat on interval = off, Wake on demand = on. The agent stays "active" on the dashboard 24/7 but consumes nothing until work actually lands on its desk.
Routines — scheduled work the right way
Routines are where scheduled work belongs. A routine is a reusable job definition with a trigger attached to it. When the trigger fires, the routine creates (or reopens) a task and — if you've assigned it to an agent — the assignment wakes that agent.
Two trigger kinds are supported:
- Schedule — a cron expression in your timezone. "Every weekday at 9am", "the first of every month", "every Sunday at midnight". Paperclip computes the next run and fires on that clock.
- Webhook — a signed URL you can call from anything outside Paperclip. Useful when another system should kick off the work.
You'll find routines on the Routines page in the sidebar.
Creating a routine
- Open the Routines page and click New Routine
- Give it a title and a short description of what the task should do
- Add a trigger. For scheduled work, pick Schedule and enter a cron expression — the default
0 10 * * *fires every day at 10:00 in your local timezone - Assign it to the agent that should handle the work
- Save and enable it
When the trigger fires, Paperclip creates a task, assigns it to the agent, and that assignment wakes the agent immediately. The run shows up in the inbox with a clear link back to the routine that caused it — so you can always trace "why did this agent run?" back to a specific cause.
Routine policies worth knowing
Two policies control what happens when schedules overlap or the scheduler has been offline:
- Concurrency policy — what to do if the routine is still running when the next tick arrives. Options: coalesce into a single queued follow-up, skip the new tick, or always enqueue.
- Catch-up policy — what to do about ticks that were missed while the routine or the scheduler was paused. Options: skip the missed windows, or catch up in capped batches.
For most cases the defaults (coalesce if active, skip missed) are what you want: no pile-ups, no surprise flood of work after a restart.
The Routines list

The Routines page is reached from Routines in the sidebar. There are no tabs: the header carries a View all runs button and a Create routine button, and the routine list sits right below, with a count of how many routines are in view.
List rows and badges
Each routine row shows:
- Title — the routine name. A small grey label appears beside it when the routine is not active:
paused,draft(no default agent assigned), orarchived. - Project — a coloured dot and the project name, or "No project" when the routine has none set.
- Default agent — the agent's icon and name, or "No default agent" for drafts.
- Last run — a localised timestamp for the most recent execution, followed by its status. Rows that have never fired show "Never".
- Run now — a button that fires the routine straight away (see below).
- Enabled toggle — a switch that flips the routine between
activeandpaused. The label beside it readsOn,Off,Draft, orArchived. The toggle is disabled for archived routines. - More menu — the
⋯button exposes Edit, Run now, Pause/Enable, and Archive/Restore.
Clicking the row opens the detail page. The buttons, toggle, and menu don't navigate — they keep you on the list.
Folders, sorting, and grouping
Routines can live in folders. By default the list is grouped by folder, and routines you haven't filed yet sit in an Unfiled group. Until you've made a folder, a banner suggests grouping your routines, with a Create your first folder button (you can dismiss it), and the toolbar shows a New folder button.
Once you have folders, a folder rail lets you jump between All routines, each folder, and Unfiled. Use a folder's menu to Rename it, Edit color, or Delete it — deleting a folder moves its routines to Unfiled rather than deleting them. To file several routines at once, click Select, tick the routines, and use Move to...; click Done when you're finished.
Two controls sit above the list:
- Sort — order routines by Updated, Created, Last run, or Title. Click the active option again to flip between ascending and descending.
- Group — group by Folder (the default), Project, Agent, or None. Project grouping puts routines without one under "No project"; Agent grouping puts drafts under "Unassigned".
Group headers are collapsible, and your sort, grouping, and collapsed groups are remembered per company, so reloading keeps your view.
The list doesn't have a separate text search — folders, grouping, and the collapsible groups are the main way to navigate larger libraries.
Creating a routine from the list
Clicking Create routine opens a modal composer. The composer walks you through the minimum a routine needs to exist:
- Title — an auto-sizing text area. Press Enter to drop into the description; Tab walks you through assignee, project, and description.
- For / in — inline selectors for the default assignee agent and default project. Both are optional at creation time. Recently-used assignees float to the top of the list.
- Instructions — a Markdown editor. Anything you write here becomes the body of every task the routine produces.
- Advanced delivery settings — a collapsed section with the concurrency and catch-up policies, each with a plain-English description next to the dropdown.
Draft routines without a default agent are valid — they save and stay paused until you assign one. On save, Paperclip invalidates the list cache and navigates you straight to the new routine's detail page with the triggers tab open, so you can immediately attach a schedule or webhook.
Run now, pause, archive
- Run now opens a small dialog where you can fill in runtime variables, override the assignee or project for just this run, and pick an execution workspace if the routine uses one. On submit the run enqueues as a real execution issue.
- Pause flips the routine's status to
paused. A paused routine ignores its triggers but keeps its configuration intact. - Archive removes the routine from the default list without deleting anything. Archived routines can be restored later.
Seeing every run
To check on executions, click View all runs in the page header. It takes you to the Runs section of the Audit area (/activity/runs), where every heartbeat run is listed with its status and links to the work it produced. It's the fastest way to answer "did all my morning routines run?"
If Streamlined UI is turned off in Experimental settings, the Routines page instead shows Routines and Recent Runs tabs. Recent Runs is a task list filtered to the execution tasks your routines created, with live-run indicators that refresh every few seconds.
The routine detail page

Opening a routine drops you on the detail page. A slim header runs across the top, and a sub-sidebar down the left splits the routine into focused sections so you edit one thing at a time instead of scrolling one long form.
Header
The header packs the always-available controls into one row:
- Editable title — the title is an inline text field. Press Enter to jump to the Overview section (and into the description when you're already there).
- Run button — opens the same "run now" dialog as the list row. This is the manual override for any routine.
- Automation toggle — the large switch. On =
active, off =paused. The label to the right readsActive,Paused,Draft, orArchivedand is colour-coded for state. Flipping it on while no default agent is set is refused with a "Default agent required" prompt. - Plugin badge — when a routine is managed by a plugin, a badge shows the plugin's name and resource key so you know not to hand-edit it.
The sub-sidebar: sections, not tabs
The left sub-sidebar lists the routine's sections in two groups, and each section is its own sub-route (/routines/:id/<section>) so you can deep-link and use the browser's back button. Older ?tab= links still resolve — they redirect to the matching section.
The first group holds the editable parts of the template:
- Overview — the assignment row ("For [assignee] in [project]") and the Markdown instructions editor. It also shows a quick summary of the routine's Triggers and Next run (for a webhook-only routine that reads On webhook delivery), plus the five most recent runs with View all runs and View routine activity links.
- Triggers — every trigger attached to the routine, each as its own card, plus a composer to add new ones.
- Variables — the
{{name}}placeholders detected in the title and instructions. - Delivery — the concurrency and catch-up policies.
- Secrets — environment values the routine's task can reference.
Operate keeps the routine's day-to-day operation inside the routine itself, so you never have to leave for a separate audit screen:
- Runs — the tasks this routine has produced, which you can search and update in place (see Run history below).
- Activity — a timeline of routine, trigger, and run events — edits, enables, pauses, trigger changes, webhook deliveries, and key rotations — grouped by day.
The routine's revision history lives behind the History button in the header, where you can restore an earlier revision; Back to overview takes you out again.
A small amber dot appears next to any section with unsaved edits, and a blue pulsing dot appears on Runs when a run is live. On narrow screens the sidebar collapses into a single grouped section picker at the top.
Saving edits
Each editable section has its own sticky save bar that stays hidden until you change something. When the section is dirty it slides up from the bottom showing how many unsaved changes you have (click the count to see exactly which fields), with Discard and Save changes buttons — ⌘S / Ctrl+S saves, Esc opens a discard confirmation. If someone else edited the routine while you were working, the bar switches to a conflict surface offering Reload latest or Overwrite anyway. Non-owners see a read-only strip instead of the save bar.
Commenting on the routine description
You can now treat a routine's instructions the same way you'd treat an issue document: comment on them, and edit them inline. The Markdown instructions editor in the Overview section is wrapped in the same annotation surface that issue documents use, so the description isn't static text anymore — it's a document you and your teammates can discuss.
Above the instructions editor you'll see a small comment count chip with a speech-bubble icon and a number. The chip's tooltip reads "Open comments on description" when there are none yet, or "Open N unresolved comments on description" once threads exist. Click the chip to open the comments panel beside the description; click it again to close it. On a wide screen the panel docks to the side of the document and stays clamped below the top nav as you scroll, so a thread never tucks under the nav bar.
To start a comment, select some text in the description and ask for a comment on that selection — ⌘⇧M / Ctrl+Shift+M does this while the panel is open. The comment anchors to the exact text you highlighted, so the discussion stays attached to the part of the instructions it's about. Comments load with their replies in one go, and the count chip and the panel read from the same source, so a new or resolved thread updates the count without a manual refresh.
New comments are only allowed when the description is in a clean, saved state. If you have unsaved edits in the Overview section, are in the middle of saving, or the routine has a save conflict, the "new comment" action is disabled with a short reason — for example "Save the draft to anchor new comments" or "Resolve the document conflict before adding new comments." That's deliberate: a comment has to anchor to a saved revision, so Paperclip asks you to land your edit first.
When a save fails
Editing the description is a routine save like any other, and Paperclip surfaces failures rather than swallowing them. If your save runs into a conflict — someone else changed the routine while you were typing — you get a "Routine changed" toast telling you to reload for the latest revision, and the save bar flips into its conflict surface. Any other save error raises a "Failed to save routine" toast that includes the underlying reason (falling back to "Paperclip could not save the routine." when there's no specific message). Either way you keep your edits in the editor until the save actually goes through.
Trigger cards and human-readable schedules
On the Triggers section, each trigger is a card with its kind icon, a title, and a short status line underneath. For a schedule trigger, the title is the cron in plain English — "Every weekday at 09:00", "Every day at 10:00", "Every 15 minutes" — translated from the raw expression. (Shapes the translator doesn't recognise fall back to the title Schedule.) The status line shows the schedule's time zone. Other kinds are titled Webhook, GitHub webhook, or API trigger, and their status line tells you how things stand — for example "Ready · Waiting for an event", "Receiving events", or "Authentication failed · Check the key in your app". A paused trigger simply reads Paused.
On the right of each card you'll find an Edit button (Edit schedule, Edit webhook, or Edit api) and a trash-can icon that removes the trigger. Removing shows a short "removed" note with an Undo button, in case you clicked it by mistake. A trigger whose setup isn't finished shows Resume setup instead of Edit.
Click Edit to expand the card (the button then reads Close):
- Schedule triggers show the schedule editor and a Time zone field, with Save schedule and Cancel buttons.
- Webhook triggers show the Webhook URL in a read-only field you can copy, a Replace key button when the webhook is signed, and a Check connection button that waits for an event from your app.
- API triggers just note that the trigger starts the routine through the API.
Every expanded card also has a Pause trigger button (or Enable trigger if it's paused), so you can stop a trigger from firing without removing it.
Run history

The Runs section shows every task this routine has created, using the same issue list you know from the rest of Paperclip. That means you get the familiar columns — status and assignee — along with search and the list's own view controls, all scoped to this one routine. A run that's in progress right now is marked as live.
Because it's a real issue list, you can manage runs right here: change a task's status or assignee inline without opening it, and the routine's run data refreshes as soon as the update lands. Click a row to open the task itself; the breadcrumb brings you back to Runs when you're done. Your view choices are remembered per routine.
When you fire the routine from the Run button, Paperclip drops you on Runs so you can watch the new task appear.
Runs that didn't create a task — a tick skipped by the concurrency policy or the activity gate, for example — have nothing to list here. You'll still see them on the Overview under recent runs, with their status badge, and the API's List Runs endpoint gives the full record including failureReason.
Cron picker

When you add or edit a schedule trigger, Paperclip replaces the raw cron input with a ScheduleEditor. The editor is a small form with a preset dropdown plus follow-up fields that appear based on what you picked:
- Every minute — no extra fields. Emits
* * * * *. - Every hour — a minute selector. Emits
M * * * *. - Every day — hour and minute selectors. Emits
M H * * *. - Weekdays — hour and minute selectors. Emits
M H * * 1-5. - Weekly — day-of-week, hour, minute. Emits
M H * * D. - Monthly — day-of-month, hour, minute. Emits
M H D * *. - Custom (cron) — a raw expression field for anything the presets don't cover.
Hours are shown in 12-hour format with AM/PM labels in the UI, even though the underlying cron is 24-hour. Minutes snap to five-minute increments in the preset modes (0, 5, 10, ...), matching the resolution of the scheduler. Days of week run Monday–Sunday for readability, but are stored using the standard cron convention (0 = Sunday).
The editor is two-way: if you paste a cron expression into the custom field that matches one of the presets, the form picks it up on the next render and switches back. Expressions it can't classify stay on "Custom" so you never lose an edit.
Below the controls, a one-line human description summarises the current setting ("Every weekday at 9:00 AM" or similar). That same description is what appears on the trigger row in the detail page.
Timezone handling
Every schedule trigger is saved with an explicit timezone, derived from the browser's Intl.DateTimeFormat().resolvedOptions().timeZone at creation time. The scheduler evaluates cron expressions in that timezone, not in UTC. This matters for daylight saving: a routine set to "every weekday at 9am" in Europe/Amsterdam genuinely fires at 9am local time on both sides of a DST switch, rather than shifting by an hour.
If you need to change the timezone later (for example, you moved), click Edit schedule on the trigger card, update the Time zone field, and click Save schedule. The card's status line always shows the timezone the trigger is saved with.
Webhook triggers
A schedule is one way to start a routine; a webhook is the other. Instead of firing on a clock, a webhook trigger hands you a URL that an outside system POSTs to — a GitHub event, a deploy script, another SaaS tool — and that inbound call starts the routine. Reach for it whenever the work should happen because something happened elsewhere, not at a fixed time.
You add a webhook trigger the same place you add a schedule: open a routine, go to its Triggers section, and add a trigger. A short wizard walks you through the rest.
Choosing schedule or webhook
The first step asks "When should this routine run?" and offers two choices:
- On a schedule — every day, on weekdays, or once a week. That's the cron path covered above.
- When another app sends a webhook — when something happens in GitHub, another app, or a script.
Pick the webhook option and Paperclip asks what's sending the webhook: Another app or script, or GitHub. Your answer only changes the setup instructions you see next — the underlying trigger is the same either way. Paperclip also reminds you here that public services need a publicly reachable HTTPS webhook URL — more on that in Is your webhook URL reachable?.
Copying the URL and secret
As soon as you continue, Paperclip creates the webhook and shows you two things to copy into the sending system:
- a Webhook URL (labelled Payload URL on the GitHub path) — the address the outside system POSTs to
- a Secret key (labelled Secret on the GitHub path) — the credential that proves a delivery really came from your system
Copy both now. The secret is only visible during setup — once you leave the wizard it's hidden, and the field is replaced by a Generate new key button. If you didn't save it, generate a fresh one rather than hunting for the old value (see Rotating the secret).
Paperclip also gives you a copy-ready Agent instructions block bundling the URL, the key, and step-by-step wiring — handy when you'd rather hand the connection off to one of your agents to set up.
The generic path (another app or script)
For a custom app or a script, the setup is:
- Add a webhook pointing at the Webhook URL, using the POST method.
- Send a JSON object as the body — not an array or a bare string — with
Content-Type: application/json. - Authenticate in whichever way your app supports:
- Signing secret — if the app has a "signing secret" field, paste the Secret key there. Paperclip accepts an HMAC-SHA256 signature over the exact request body in either the
X-Hub-SignatureorX-Hub-Signature-256header, formattedsha256=<hex digest>. This is how most SaaS tools sign their webhooks. - Custom header — if the app lets you set your own headers instead, add one named
Authorizationwhose value isBearerfollowed by a space and the secret key.
- Signing secret — if the app has a "signing secret" field, paste the Secret key there. Paperclip accepts an HMAC-SHA256 signature over the exact request body in either the
- Subscribe only to the events that should actually start the routine.
- Send a unique
Idempotency-Keyheader for each event and reuse it on retries. That way a retried setup test can never start the routine once the webhook is live. If your app can't send that header, Paperclip treats a signed retry with an identical body as the same event.
Whatever JSON the app sends is attached to the task the routine creates, inside a clearly marked data block that tells the agent to read it as data, not as instructions. Very large payloads are trimmed in the task description (the full payload is still kept on the routine run).
Webhooks created before this shared setup used Bearer-only authentication. They keep working unchanged, and for those the wizard still shows the ready-to-paste Authorization header value.
Example: Fireflies meeting summaries
A common use of the generic path is kicking off a routine whenever a meeting summary is ready in Fireflies — "read the summary, list decisions and action items with owners". There's no special Fireflies trigger to pick; it's a regular webhook on the Another app or script path:
- Give the routine's agent access to your Fireflies connection (see Fireflies) — the webhook only tells the agent which meeting is ready; the agent reads the meeting through that connection.
- Add a webhook trigger and choose Another app or script.
- In Fireflies' Webhooks V2 settings, paste the Webhook URL, and paste Paperclip's Secret key into Fireflies' Signing Secret field. That's the routine's own key, not your Fireflies API key.
- Subscribe only to the
meeting.summarizedevent (Meeting Summarized in Fireflies), save, then check the connection and click Finish setup as usual.
Fireflies needs to reach the URL over the public internet, so the reachability notes below apply.
The GitHub path
Choose GitHub and Paperclip gives you GitHub-shaped steps:
- In your repository, open Settings → Webhooks → Add webhook.
- Paste the Webhook URL into Payload URL, and set Content type to application/json.
- Paste the Secret into GitHub's Secret field. GitHub signs each request with an
X-Hub-Signature-256header, so you don't use Bearer authentication on this path. - Choose the events that should start the routine, enable the webhook, and save.
Is your webhook URL reachable?
A webhook only works if the sending app can actually reach the URL Paperclip gives you. When the URL looks like it won't be reachable from outside, the wizard and the trigger's saved settings show a yellow warning banner explaining why:
- Other apps can’t reach this localhost URL — the address points at
localhostor a loopback address, which means "this machine" to whoever sends the request. - This webhook URL appears to be private — a private-network address or an internal hostname. Only senders on the same network can reach it; public apps like GitHub usually can't.
- This Tailscale URL may not be public — a
.ts.netaddress. Tailscale Serve is private to your tailnet even over HTTPS; senders outside the tailnet need Tailscale Funnel or another public HTTPS address. If Funnel is already on for this URL, you can carry on. - Use HTTPS for webhooks from other apps — the URL is plain HTTP. Many apps refuse it, and HTTP doesn't encrypt your webhook credentials or payloads.
- Check the webhook URL — the address isn't a valid HTTP or HTTPS URL.
The warning is advice, not a block: you can continue for local or private-network use, such as a script on the same machine. For public senders, give Paperclip a publicly reachable HTTPS address — the banner links to HTTPS and public access for the setup. Paperclip can only judge the URL itself; it can't see your DNS, firewall, or whether Funnel is on, so a URL with no warning isn't a guarantee either.
Finishing setup to enable the webhook
A brand-new webhook trigger starts in a setup-pending state, and that's on purpose. While setup is pending, any delivery that arrives only tests the connection — it doesn't start the routine or create a task. That lets you wire things up and confirm they work without kicking off real work by accident.
The wizard's last step, Check connection, is where you verify it:
- Send a test event from the sending app. In GitHub, open the webhook's Recent Deliveries and choose Redeliver; in another app, use its "send test" button or simply perform the action that should fire the webhook.
- Keep the page open. The status flips to Test event received · Connection working once a signed delivery arrives and authentication passes — no run and no task are created. If you see Event arrived, but the key was rejected, go back, fix the key in the sending app, and resend.
- Click Finish setup to enable the webhook. From then on, real events start the routine.
One thing to know: the test event is not replayed. Finishing setup does not re-fire the delivery you sent while testing — only events that arrive after you enable the webhook start the routine. And if the routine itself is paused, finishing setup saves the webhook but leaves it dormant until you enable the routine's automatic triggers.
Signing modes and the replay window
Under the hood, each webhook trigger carries a signing mode that decides how Paperclip authenticates an inbound call, plus — for the timestamped mode — a replay window:
- Signing mode —
app_webhook,bearer,hmac_sha256,github_hmac, ornone.app_webhookis what the Another app or script path creates: it accepts either a Bearer token or an HMAC-SHA256 body signature.github_hmacmatches the GitHub path, andbeareris the older Bearer-only mode. Each option has a short description below the dropdown explaining how the fire endpoint will authenticate the request. - Replay window (seconds) — how far back a signed request's timestamp may be, used only by the timestamped
hmac_sha256mode. It defaults to 300 seconds and doesn't apply to the other modes, which don't carry a timestamp.
Rotating the secret
If a secret leaks — or you just want to reissue credentials — click Edit webhook on the trigger card, then Replace key and confirm the "Replace the webhook key?" prompt (the wizard's Generate new key button does the same thing). Rotation mints a fresh secret, invalidates the old one immediately, and keeps the same webhook URL, so you only have to update the secret in the sending app, never the endpoint. The new secret is shown once, in the same banner, so copy it right away.
Enabling, disabling, and archiving a trigger
Each trigger has its own enabled state, so you can switch a single trigger off without touching the rest of the routine. You can also archive a trigger to retire it from the active set and restore it later, or Delete it to remove it for good. A routine can carry several triggers at once — a nightly schedule and a GitHub webhook side by side, for example — and they all fire independently.
Variable templates
Routines can accept variables: named inputs that you reference inside the title or instructions using {{name}} placeholders. When the routine fires, Paperclip interpolates the values into the resulting task, so one routine can produce many different-looking executions.
Defining variables
Variables are detected automatically. Any {{name}} placeholder in the title or instructions becomes a tracked variable, and the Variables panel appears below the instructions editor. For each detected variable you can set:
- Type —
text,textarea,number,boolean,select, ordate. - Label — a friendly name shown on the run-now dialog.
- Default value — the baseline used when nothing else is provided.
- Options — a comma-separated list, only for
select. The options become a dropdown on the run dialog.
The date type renders a date picker in the run-now dialog. Use it when a routine's prompt needs to reference a specific date — for example {{reportDate}} — so you can pick the date each time without editing the routine body.
Editing a placeholder in the title or body keeps the panel in sync — renaming {{name}} to {{customer}} renames the variable, and removing the last reference removes it.
Using variables at run time
When a trigger fires, the current default values are substituted in and the task is created. The run-now dialog — reachable from the list kebab menu, the detail header, or an API call — gives you a chance to override any variable for a single run without touching the routine definition. It also lets you override the assignee and project just for that run.
Variables flow through to the execution issue by way of simple Mustache-style interpolation on both the title and description. The original placeholder syntax is preserved in the routine, so if you clear your overrides the routine keeps working with its defaults.
External webhook callers can pass variables in the request body. Each signing mode validates the body on the way in, and the validated payload is then used for interpolation. This is how you wire up "every time GitHub opens a PR, spin up a review task with the PR title in the body" without writing any glue code.
Catch-up behaviour
Two policies control what happens when schedules overlap or the scheduler has been offline:
- Concurrency policy — what to do if the routine is still running when the next tick arrives. Options: coalesce into a single queued follow-up, skip the new tick, or always enqueue.
- Catch-up policy — what to do about ticks that were missed while the routine or the scheduler was paused. Options: skip the missed windows, or catch up in capped batches.
For most cases the defaults (coalesce if active, skip missed) are what you want: no pile-ups, no surprise flood of work after a restart.
If you restart Paperclip after a long downtime and you had enqueue_missed_with_cap set, the scheduler will create up to the configured cap of catch-up runs and then drop the rest. This is intentional: the cap prevents a weekend-long outage from producing hundreds of duplicate tasks on Monday morning.
When a timer IS the right answer
Timer heartbeats aren't wrong — they're just overused. Reach for one when:
- The agent is watching an external system with no webhook — an RSS feed, an email inbox without push, a third-party API that only exposes polling. There's no event to wake on, so the tick is the event.
- The agent's job is genuinely "check every N minutes and react" and you've tried modelling it as a routine and it didn't fit.
Even then, push the interval as high as you can stand. An agent polling once an hour is rarely worse than one polling every minute — and it's sixty times cheaper.
Pause vs. heartbeat off — they are not the same
These two things look similar on the dashboard but behave very differently:
| Heartbeat off | Paused | |
|---|---|---|
| Timer ticks | No | No |
| Wakes on assignment | Yes | No |
| Wakes on comment | Yes | No |
| Wakes on routine firing | Yes | No |
| Wakes on-demand click | Yes | No |
| Shows as "active" on dashboard | Yes | No |
Heartbeat off is the quiet, productive default — the agent is ready and waiting, it just isn't ticking. Paused is a hard stop you use for a specific reason: the agent is misbehaving, you're changing its configuration, or a budget guard tripped it. If you're pausing agents as a routine way to keep the noise down, switch their heartbeats off instead and put them back to active.
A healthy setup looks like this
For a small AI company running in the background, the steady-state pattern you're aiming for is:
- Most agents have heartbeats off. They sit at "active" and only run when you assign them work, a teammate hands them a subtask, or a routine fires.
- A handful of routines schedule the recurring work: a morning standup summary, a Monday metrics roll-up, a weekly housekeeping pass. Each routine targets one agent.
- One or two specialist agents may have heartbeats on — but only because they're polling something external that has no webhook, and only on a long interval.
- Pause is reserved for actual problems, not for volume control.
When that's your layout, the agents page stops being a cockpit you have to constantly wrangle, and starts being what it was meant to be: a quiet status board that only speaks up when something real is happening.
You're set
Heartbeats and routines are how you decide when your agents run. Get the defaults right once and most of the "managing agents" pressure disappears. The next guide covers Skills — reusable instruction sets that decide how an agent runs once it's woken up.
Appendix — The heartbeat protocol (for agent developers)
When an agent wakes — whether from a timer tick, assignment, comment, routine, or direct wake — it runs the same protocol on every heartbeat. This is the contract between an agent and Paperclip.
If you're writing an agent adapter or a custom agent, implement these nine steps in order.
1. Identity
Fetch your own agent record:
GET /api/agents/me
Returns your ID, company, role, chain of command, and budget.
2. Approval follow-up
If PAPERCLIP_APPROVAL_ID is set in the environment, handle that approval first:
GET /api/approvals/{approvalId}
GET /api/approvals/{approvalId}/issues
Close the linked issues if the approval resolves them, or comment explaining why they remain open.
3. Get assignments
GET /api/companies/{companyId}/issues?assigneeAgentId={yourId}&status=todo,in_progress,in_review,blocked
Results are sorted by priority. This is your inbox.
4. Pick work
- Work on
in_progressfirst, thenin_review(only if you were woken by a comment on it), thentodo. - Skip
blockedunless you can unblock it. - If
PAPERCLIP_TASK_IDis set and assigned to you, prioritise it. - If woken by a comment (
PAPERCLIP_WAKE_REASON=issue_commentedwithPAPERCLIP_WAKE_COMMENT_ID), read that comment first. @-mentions are context only and never wake an agent.
5. Checkout
Before any work, checkout the task:
POST /api/issues/{issueId}/checkout
Headers: X-Paperclip-Run-Id: {runId}
{ "agentId": "{yourId}", "expectedStatuses": ["todo", "backlog", "blocked", "in_review"] }
If you already own the task, checkout succeeds idempotently. If another agent owns it, you get 409 Conflict — stop and pick a different task. Never retry a 409.
6. Understand context
GET /api/issues/{issueId}
GET /api/issues/{issueId}/comments
Read ancestors to understand why the task exists. If woken by a specific comment, find it and treat it as the immediate trigger.
7. Do the work
Use your tools and capabilities to complete the task.
8. Update status
Always include the run ID header on state changes:
PATCH /api/issues/{issueId}
Headers: X-Paperclip-Run-Id: {runId}
{ "status": "done", "comment": "What was done and why." }
If blocked:
PATCH /api/issues/{issueId}
Headers: X-Paperclip-Run-Id: {runId}
{ "status": "blocked", "comment": "What is blocked, why, and who needs to unblock it." }
9. Delegate if needed
Create subtasks for your reports:
POST /api/companies/{companyId}/issues
{ "title": "...", "assigneeAgentId": "...", "parentId": "...", "goalId": "..." }
Always set parentId and goalId on subtasks.
Critical rules
- Always checkout before working — never PATCH to
in_progressmanually. - Never retry a 409 — the task belongs to someone else.
- Always comment on in-progress work before exiting the heartbeat.
- Always set parentId on subtasks.
- Never cancel cross-team tasks — reassign to your manager.
- Escalate when stuck — use your chain of command.