Build a scheduled Workflow
Say you want every @team-exec mention in Slack turned into a well-formed Linear issue, without a person watching the channel. This guide builds that as the canonical scheduled pipeline: a cron trigger, a deterministic Slack search, a cheap model summary per message, a Linear issue per summary, and a durable cursor so nothing is processed twice.
Before you start
- Workspace-scoped Slack and Linear connections, healthy enough to have cached their tool lists. Tool steps run on workspace connections — a personal connection can't back an unattended run.
- Familiarity with how a Workflow's trigger and steps fit together — see Workflows.
The shape
Only one step in this job needs a model, and none of it needs a full Agent:
- Trigger: Schedule —
*/20 * * * *, every 20 minutes. - Tool (
search) — call the Slack connection's search tool for@team-execmentions newer than@state.cursor. - For each over
@steps.search's messages, per message:- Infer (
summarize) — a prompt on thequickpreset that turns@iteminto an issue title and description, as declared structured output. - Tool (
file_issue) — call the Linear connection's create-issue tool, its args templated from@steps.summarizeplus the message's permalink from@item.
- Infer (
- State — advance
cursorto@now, so the next run only sees what's new.
Steps
- Create a Workflow and open the workflow editor. The fastest path is the copilot composer: describe the job in a sentence or two — for a never-published draft the copilot applies its steps as it goes, and it looks up the real Slack and Linear tool names from your connections rather than guessing. The rest of these steps are the direct-manipulation equivalent, and either way you should end up with the same strip.
- On the Trigger card, pick Schedule and enter
*/20 * * * *. The live preview reads back what it understood, with a Valid / Unrecognized status chip. - Add a Tool step: pick the Slack connection, pick its search tool from the tool picker, and template the query with
@state.cursorfor the "newer than" bound. On the first run the cursor doesn't exist yet — an unset@statekey just resolves empty. - Add a For each step over the search step's message list (
@steps.search.…). - Inside its body, add an Infer step: the
quickpreset, a prompt over@item, and a declared output schema (title,description) so the next step gets fields, not prose. - Still inside the body, add a Tool step on the Linear connection's create-issue tool, with args referencing
@steps.summarize.result.title,@steps.summarize.result.description, and the permalink from@item. - After the loop, add a State step that sets
cursorto@now. - Click Publish — instant, since a Workflow builds nothing.
Before publishing, use Test step on the search step to see the real tool's real output shape — that's how you find the exact path for the For each's list. (Tool tests have real side effects; a search is safe, the create-issue step less so.)
Why the cursor works
Ordering does the heavy lifting. The cursor only advances after the loop, so a run that fails mid-way never claims messages it didn't process — the next run re-reads them. And because a scheduled trigger skips a tick while the previous run is still live rather than overlapping it, two runs can never race the same cursor. The loop body is iteration-scoped on purpose: each message's summary is visible to its own issue-filing step and gone after that iteration, so there's no cross-item leakage.
UTC only
Schedules run on the platform clock, which is UTC — always, with no per-workspace timezone setting. A cadence like */20 * * * * doesn't care, but a clock-anchored expression like 0 9 * * 1-5 means 09:00 UTC: if you want 09:00 US/Eastern you must write the UTC equivalent yourself, and revisit it when daylight saving shifts the offset — the platform has no way to know you meant "9am wherever I am."
No backfill
Ticks missed while the platform is down are skipped, not queued up and replayed. That's the right default here: the next successful run's search picks up from the cursor anyway, so nothing is lost — it just arrives on the next tick instead of as a burst of catch-up runs.
Test without waiting
Open the header's Run popover and press Fire now to dispatch immediately through the same pipeline path the ticker uses. One small difference: a real tick stamps trigger.scheduledFor (the fired window) into the trigger data, while a manual fire carries an empty payload — so a pipeline that references @trigger.scheduledFor sees "(not provided)" on manual runs. The pipeline pane overlays live per-step progress, and View run opens the full timeline in the Runs tab: each step's rendered input and output, per-iteration instances under the loop, and the state write at the end.
If it doesn't work
- The status chip reads "Unrecognized" — the cron syntax is wrong. It needs exactly five numeric fields; month and day-of-week names aren't accepted, only numbers.
- Nothing fired at the expected time — check the UTC conversion first, then confirm the Workflow is actually published. A draft schedule doesn't tick. If the previous run was still going, the tick was skipped for overlap — check the Runs tab.
- The For each fails with an empty or wrong list — the
itemsreference doesn't match the search tool's actual output shape. Test the search step and read its real output. - The same message was processed twice — a run crashed after filing its issue but before the cursor advanced; at-least-once is the deliberate trade. See Durability.
See Triggers for how the other trigger kinds compare, Sessions & runs for how a scheduled dispatch shows up as a run, and troubleshooting for anything not covered above.