routines
When the user asks for anything recurring, scheduled, or event-driven, such as a reminder, digest, monitor, "let me know when", or a change to an existing routine, read before you create or change it.
SKILL.mdnon-stock/home/box/agent-data/managed-skills/skills/routines/SKILL.md
GitHubRoutines
A routine is a saved prompt plus a trigger, created and changed with the update_state tool (target "routine"). This is the full recipe behind the short Routines section of your prompt; the live list of the user's routines stays in that section.
Write the prompt as an intent, not a frozen tool recipe: don't bake specific MCP tool call arguments or schemas into it. A connector's schema can change between fires, so describe what to do and let each run look the tool up with your MCP discovery tool (GetMcpTools, or GetDynamicTools when that is the one you hold). Store communication intent rather than names such as SendMessage, SendToUser, or SendToAgent. If the routine must ping, remind, tell, notify, ask, or otherwise say something to the user or anyone outside the run, state that required outcome explicitly; the runtime chooses the available route.
schedule is a 5-field cron expression interpreted in the user's local time (the timezone named in your prompt) ("minute hour day-of-month month day-of-week"), e.g. "47 6 * * " = every day at 6:47am, "32 * * * " = hourly, at :32 past each one, "15 9 * * 1" = 9:15am every Monday, "12 9 * * 1-5" = 9:12am on weekdays, "32 9-17 * * 1-5" = hourly through the weekday workday, "17,47 9-17 * * 1-5" = every half hour through the weekday workday. The Routines section of your prompt states the fastest accepted interval when a floor applies; never schedule below it. The shorthands @hourly/@daily/@weekly/@monthly and "@every 5m|2h|1d" also work. Calendar shorthands take their clock fields from the routine's creation time, and an unphased @every interval anchors to creation. To pin a schedule to a fixed timezone instead of following the user's, prefix it with "CRON_TZ=<IANA zone> ", e.g. "CRON_TZ=America/New_York 15 9 * * ".
For scheduled routines, choose the cadence and delivery time around when the result will be valuable, especially when the user is likely to read or act on it, rather than maximizing how often the routine runs. Prefer natural, coarse boundaries such as a morning digest, an hourly check, or a weekday reminder over constant polling. Start with the least-frequent schedule that still delivers the intended value, and tighten it only when delay has a real cost.
The <timestamp> on the user's latest message is the routine's creation time, and every minute field you write comes from it. The one exception is a minute the user names, which is saved exactly as named. For example, "9:30" is "30 9 * * " and "quarter past 8 on weekdays" is "15 8 * * 1-5". Moving an existing routine to a time they name works the same way. A named hour with NO minute ("8am", "daily at 2", "weekdays at 9") is not a request for the top of that hour: land it within 15 minutes either side of that hour on the timestamp's minute, and tell the user the exact time you saved. A timestamp minute of :45–:59 goes on the hour before (asked at 1:47, "8am" is "47 7 * * ", 7:47am, and "weekdays at 9" is "47 8 * * 1-5"); :00–:15 goes on the named hour (asked at 1:12, "8am" is "12 8 * * " and "daily at 2" is "12 2 * * "); :16–:44 is more than 15 minutes out, so shift it by 30 into the window (asked at 1:32, "8am" is "2 8 * * "; asked at 1:20, "50 7 * * "). If they push back, or clearly mean the exact minute ("8 sharp", "on the hour", "exactly 9"), save the minute they name, top of the hour included.
The same timestamp minute fills every ask that names no clock time, such as "hourly", "every hour", or a loose "check daily" where you pick the hour. It ALSO fills every interval window. Asked at 1:47, "hourly" is "47 * * * ", hourly through the workday is "47 9-17 * * 1-5", a daily check lands at "47 8 * * 1-5", and "every half hour during the workday" is "17,47 9-17 * * 1-5". Never write "/30" or "0,30" in the minute field. Write an interval as an explicit minute list stepped from the timestamp minute; "/N" counts from :00 by construction and is never what you want. A minute field of 0 or 30 (or a "/N" that lands on them) appears only when the user explicitly asked for that exact minute; when the timestamp itself reads :00 or :30, step a minute or two off it. If you ever save an "@every" interval instead, phase it off the mark too ("@every 30m/17m"). The bounded explicit cron is still preferred.
Weekdays and waking hours are the DEFAULT window for a scheduled routine, not one consideration among many. Pin BOTH the day-of-week and the hour instead of leaving either as "": weekdays are "1-5" and a daytime window runs from about 8am to about 7pm in the user's zone. Examples are "47 8 * * 1-5", "47 9-17 * * 1-5", and "17,47 9-18 * * 1-5", with the same asked-at 1:47 as the line above rather than a fixed minute. Bounding one field and leaving the other open is the half-measure to avoid: an hour range with day-of-week "" still runs all weekend, and weekdays with hour "" still fires at 3am. Roughly 10pm to 7am local is quiet hours and Saturday/Sunday is off. Use the user's real hours when you actually know them (from memory, their calendar, or their own words); otherwise assume a normal weekday morning-to-evening window.
That default binds hardest on the vaguely-worded ask. "Check daily", "every day", "keep an eye on it", "remind me", "every half hour" are loose phrasing for "regularly", not requests for round-the-clock coverage. People say "daily" without meaning Saturday, so it does not by itself justify a weekend or overnight fire. The shorthands quietly deliver exactly that: @daily fires every day at the routine's creation time, @hourly fires all night at its creation minute, and "@every 30m" cannot be restricted to any window at all. Translate the loose ask into a bounded cron instead of saving the shorthand as-is: "47 8 * * 1-5" rather than @daily, "17,47 9-17 * * 1-5" rather than "@every 30m".
Leave the window only for a reason you could say out loud, and name that reason in the same breath as the schedule, so an off-hours routine is always a stated choice rather than a leftover "". Real reasons: the user was unmistakably explicit ("including weekends", "weekends too", "7 days a week", "every single day"); the subject is genuinely time-critical (an incident, a deploy, a deadline that can pass overnight); the thing being watched only happens then (an overnight batch, a weekend trip); or the routine concerns the user's own life rather than their office. Examples are a medication or health reminder, pet care, a daily habit or streak, and weekend plans. These routines should cover all seven days, since skipping Saturday there is the bug. Note that a feed which keeps producing around the clock is NOT such a reason: what matters is when the user is there to act on it.
For an event-driven routine, pass a "trigger" INSTEAD of a "schedule". Trigger shapes:
{ "type": "slack", "channel": "#eng" | "@someone" | "", "match": { "kind": "mention" } | { "kind": "keyword", "keyword": "deploy" } | { "kind": "message" } | { "kind": "reaction" } }
A reaction match also takes two optional filters: "emoji" (short names without colons, e.g. { "kind": "reaction", "emoji": ["eyes", "pencil2"] }, and any one of them fires it; omit for any reaction) and "bySelf": true (only the user's OWN reactions, not a colleague's). Reach for both together with "channel": "" when the user wants their own emoji to be the signal: "when I react :eyes: to anything, do X".
{ "type": "github", "repo": "owner/name" (one concrete repo, no wildcard), "events": ["pr-opened" | "pr-pushed" | "pr-merged" | "pr-closed" | "review-requested" | "review-approved" | "review-changes-requested" | "review-commented" | "pr-comment" | "inline-review-comment" | "review-thread-resolved" | "review-thread-unresolved" | "issue-assigned" | "ci-passed" | "ci-failed", ...], "pr"?: 123 (OPTIONAL; only that pull request), "userAllowlist"?: ["octocat", ...] (OPTIONAL git logins, "@" optional; omit or leave empty for anyone), "ciBranch"?: "main" (REQUIRED for CI events when pr is absent) }
userAllowlist filters the github listener to events involving those git users; omit it (or leave it empty) to fire for anyone. The gated user is per event kind, matching who drives it: the PR author for pr-opened/pr-pushed/pr-merged/pr-comment/inline-review-comment; BOTH the actor AND the PR author for review-approved/review-changes-requested/review-commented/review-thread-resolved/review-thread-unresolved/review-requested; the assigner for issue-assigned; and it does NOT apply to ci-passed/ci-failed (CI is never user-gated). So "PRs I open" is the user's own login on the pr- events, and "reviews on my PRs" is the user's login on the review-* events. Use the user's actual GitHub login (confirm it, e.g. with gh api user, rather than guessing from their display name).
For a repo subscription, omit pr: { "type": "github", "repo": "owner/name", "events": ["pr-opened", "pr-merged"] }. pr-pushed, pr-comment, inline-review-comment, and review-commented are refused repo-wide; they need pr or userAllowlist.
For "subscribe to / babysit this PR", use this default pack: { "type": "github", "repo": "owner/name", "pr": 123, "events": ["review-requested", "review-approved", "review-changes-requested", "review-commented", "pr-comment", "inline-review-comment", "review-thread-resolved", "review-thread-unresolved", "pr-pushed", "pr-merged", "pr-closed", "ci-passed", "ci-failed"] }. With pr, CI means that PR's checks and ciBranch is unnecessary. Including pr-merged and pr-closed makes the routine delete itself after either terminal wake finishes. Without pr, CI remains branch-scoped: ciBranch names the ONE branch whose settled checks fire ci-passed / ci-failed. Ask which branch the user means (usually "main") rather than guessing.
{ "type": "origin", "repo": "owner/name" (one concrete NATIVE Origin repo, and a mirrored repo is rejected), "events": ["pr-opened" | "pr-pushed" | "pr-merged" | "review-requested" | "review-approved" | "review-changes-requested" | "review-commented" | "pr-comment" | "inline-review-comment" | "review-thread-resolved" | "review-thread-unresolved" | "ci-passed" | "ci-failed", ...], "pr"?: 123, "userAllowlist"?: ["<Origin actor id>", ...] }
Origin userAllowlist accepts actor IDs in either user-facing or internal numeric form. It gates PR/comment events by the PR owner; review/reviewer/thread events require BOTH the actor and PR owner; CI ignores the allowlist. Missing required identities fail closed.
For "subscribe to / babysit this Origin PR", use this default pack: { "type": "origin", "repo": "owner/name", "pr": 123, "events": ["review-requested", "review-approved", "review-changes-requested", "review-commented", "pr-comment", "inline-review-comment", "review-thread-resolved", "review-thread-unresolved", "pr-pushed", "pr-merged", "ci-passed", "ci-failed"] }. Origin CI is PR-only: ci-passed / ci-failed are discarded unless pr is set. Including pr-merged makes the routine delete itself after its merged wake finishes. Use github only for GitHub repos and origin only for native Origin repos; never point origin at a GitHub mirror.
{ "type": "microsoftTeams", "tenantId": "<Microsoft Entra tenant id>", "teamIds": ["<Graph API team id>", ...], "channelIds"?: [...] (omit for every channel), "messageContains"?: "deploy" (omit for any message) }
{ "type": "linear", "event": { "case": "issueCreated" } | { "case": "statusChanged", "statusIds"?: [...] } | { "case": "endOfCycle", "cycleIds"?: [...] }, "projectIds"?: [...], "teamIds"?: [...] }
{ "type": "sentry", "event": { "case": "issueCreated" | "issueResolved" | "issueAssigned" | "issueArchived" | "issueUnresolved" | "issueAny" }, "projectIds"?: [...] }
{ "type": "pagerduty", "event": { "case": "incidentTriggered" | "incidentAcknowledged" | "incidentResolved" | "incidentEscalated" | "incidentAny" }, "serviceIds"?: [...] }
{ "type": "email", "inbox": "<one of the user's Grok Bot inbox addresses (list_email_inboxes)>", "from"?: ["sender@example.com", ...] (omit for any sender), "requireAuthPass"?: true (default; false also wakes on mail that failed SPF/DKIM/DMARC) }, which fires when mail arrives at that inbox. Mail matching no routine wakes nobody. The wake carries headers only; read the body with read_email_thread.
{ "type": "webhook" }, which fires when an outside system POSTs to this routine's webhook URL. The user copies the URL and its sender key from the routine panel; you never see or need the key. To point them at a field, paste its ready-made link from the routine's line under Current routines.
The id arrays on the linear/sentry/pagerduty shapes, and a microsoftTeams channelIds, are optional narrowing filters (platform ids/UUIDs); omit one to fire for any project, status, cycle, channel, or service. A microsoftTeams trigger always names its scope: tenantId plus at least one team id (teamIds) are required.
{ "type": "group", "listeners": [ ...several listeners... ] }, where any one fires the same prompt. Origin can group with cron, Slack, and GitHub; do not group it with Microsoft Teams, Linear, Sentry, PagerDuty, email, or webhook until those triggers share one scheduling authority.
Prefer an event-driven trigger over a cron schedule when the event the user cares about is represented by one of the listener shapes above. Do not poll on a timer for Slack messages, mentions, keywords, reactions, or the listed GitHub, Origin, Microsoft Teams, Linear, Sentry, or PagerDuty events unless a finite watch must enforce a deadline even if the event never arrives; listeners do not wake just because time passed. For that deadline-enforcement case, create a cron-only routine instead of a listener. Never pass both trigger and schedule. Use cron for genuinely time-based work, unavailable events, or that deadline-enforcement case.
When a listener fires, the wake includes the triggering event in a block named for its source (<slack_message>, <github_event>, <origin_event>, <microsoft_teams_message>, <linear_event>, <sentry_event>, <pagerduty_event>, <email_received>, <webhook_event>). That is WHAT woke you; act on it with the saved prompt.
Event listeners fire through the user's Cursor account connections (the same ones cloud-agent automations use), never a token pasted into Grok Bot, and never a token you ask the user for. If saving a listener routine reports that the platform isn't connected, its connect card is shown to the user automatically; just say so and carry on.
A Slack CHANNEL listener ("#eng") only hears channels the Cursor Slack app is actually in. Whenever you create one, and whenever a channel listener seems dead, tell the user to invite @Cursor to that exact channel in Slack (type /invite @Cursor in the channel); a private channel can't even be found until the bot is invited. The Routine panel flags affected channels the same way, so don't let a silent listener pass without mentioning the invite. The invite advice does not apply to a DM ("@someone") listener, but it does apply to "": a "" listener hears every channel the app is in, so an uninvited channel is silent there too.
Make every short-lived, finite, or conditional watch ("keep an eye on X", "ping me when Y", "watch this until it merges", "for a bit") self-expiring by default. A PR-scoped GitHub listener that includes pr-merged or pr-closed is deleted automatically after that terminal wake finishes; a PR-scoped Origin listener that includes pr-merged is deleted after its merged wake. Do not delete either on earlier review, comment, push, or CI events. For a scheduled watch, put a concrete deadline in its saved prompt and delete it after reporting the watched condition or as soon as a run finds that the deadline has passed. For other event-driven one-shot watches, delete after handling the matching terminal condition. If it must disappear by a deadline even when no event arrives, make it a cron-only scheduled routine instead of a listener; never combine trigger and schedule in one routine. A permanent routine is appropriate only when the user explicitly wants an ongoing result such as a daily digest, weekly reminder, or standing Slack/GitHub/Origin subscription.
If you can't authenticate to carry out a routine because an integration, MCP connector, or tool it depends on rejects you for auth (not connected, token expired, access revoked), check whether you already hit that same auth failure on an earlier run of this routine. Your own earlier messages in this conversation are the record; a gracefully-handled auth failure still leaves the run marked "succeeded", so don't rely on run status to notice the repeat. A one-off first failure is fine to just report, but once the same auth block is clearly recurring, stop firing blindly and re-reporting it on every trigger: pause the routine (update_state action "pause") and tell the user what to reconnect. When it is an MCP connector (a needsAuth server), call AuthenticateMcpServer for it, and its connect card is shown automatically so the user re-authorizes in place; for anything else, send a normal SendToUser naming exactly what needs reconnecting. Resume it (action "resume") once the connection is fixed, or leave it paused for the user to re-enable.
Creating or changing a routine may ask the user to confirm before it saves, since a routine is the one thing you set up that acts while they're away. If it does, they see a card with the schedule and the instruction, and their answer comes back as your tool result, so don't ask for permission yourself first, and don't retry a denied write with reworded text.
Situations that should usually become a routine (transient where it ends on a condition, durable where it recurs):
- Surface Slack messages, mentions, keywords, or reactions with a Slack listener: keep an ongoing subscription durable, or delete a one-shot listener after its first match.
- React to GitHub events with a listener: keep an ongoing repo subscription durable; a PR-scoped watch that includes pr-merged or pr-closed removes itself after that terminal wake.
- React to native Origin events with an Origin listener: keep an ongoing repo subscription durable; a PR-scoped watch that includes pr-merged removes itself after the merged wake.
- Deliver a weekday morning digest shortly before the user is likely to read it: calendar, unread email, and overnight alerts or news. This one is durable.
- Monitor a dashboard, metric, or error rate at the coarsest useful cadence, inside the user's weekday hours unless it genuinely matters overnight; alert only when the result is actionable. This one is durable when ongoing.
- Use an event trigger for a long-running job, deploy, or CI completion when one is supported; otherwise check at a low useful cadence. Delete a finite watch after completion and, when scheduled, at its deadline. This one is transient.
- Send a recurring reminder at the natural time to act (for example, Monday morning rather than overnight or all weekend). This one is durable.
- Watch an inbox, queue, or ticket using an event trigger when its event is supported; otherwise check only as often, and inside the weekday hours, needed to surface useful new items.