GTM Agent schedules
A schedule runs the GTM Agent on a cadence. Each occurrence starts a fresh
agent task with the schedule's prompt as its first message, run as the
schedule's creator. Nothing carries over between occurrences — no conversation,
no working files — so a schedule prompt must stand alone: name the audience,
table, or workflow it works on explicitly, and say what the output should be.
The commands
bashclay schedules create --input '{...}' # create; starts active with nextRunAt computed clay schedules list # the schedules you created, status, cadence, next run clay schedules update <scheduleId> --input '{...}' # partial update; also pause/resume via status clay schedules delete <scheduleId> # remove it and every run it produced
Read clay schedules <subcommand> --help for the exact input and output
shapes; they are the contract.
Writing the prompt
The prompt is the entire brief every occurrence gets. Write it as exactly three
short sections, in this order:
- Objective — one sentence: the ongoing job this schedule exists to do.
- Goal — the measurable end state, and any hard limits (spend, volume). When the goal can be measured from workspace data, state it in those terms and name the measure — the audience id, filter, or activity types a run should count (see "Measuring the goal") — so a run can tell from the reading alone whether it is done.
- Instructions — a short numbered list of what one occurrence does, naming every audience, field, and workflow by id (a fresh task cannot infer them).
Write the Instructions for the minimum work that makes progress toward the goal, not for finishing it in one run. When the Goal names a measure, the first step is taking it; the reading then decides the rest — a goal at its target means skip that work entirely, and a backlog means work a bounded batch of it (name the batch size), report the remainder, and stop. The schedule is the loop; no single occurrence needs to be.
Keep it brief — a few hundred words, well under the 4000-character cap. A prompt near the cap is a smell that it has absorbed things that do not belong in it:
- Tool usage and CLI flag reference — the run reads the same skills you did.
- Cadence. Frequency, days, run time, time zone, and start and end dates live in
scheduleConfigand are enforced from there alone, so a prompt that restates them is wrong the moment the two differ: "every Monday" on a daily cadence runs seven days a week, and every run believes it is Monday's. Time windows that describe the work — "leads since the last run", "last week's pipeline" — are not cadence and stay. - Restated notification or credit policy. Rely on the shared cost policy in
workflows-discover-actions/cost-and-budget.mdrather than adding balance disclosures or repeated spending approvals to the schedule prompt. - Contingency trees. A run that hits a blocker reports it and stops; the next occurrence retries. One line covers it.
- Report formatting specs. Say what facts the final reply must contain (the goal's current reading, what changed, what failed) and leave the prose to the run.
The cadence
scheduleConfig is one of two recurrence kinds:
json{ "recurrenceType": "simple", "value": { "periodUnit": "daily", "startDate": "2026-09-01T09:00:00", "timezone": "America/New_York" } }
periodUnit is one of daily, weekly, biweekly, monthly, quarterly.
json{ "recurrenceType": "custom", "value": { "customPeriodType": "weeks", "customPeriodInterval": 1, "timesOfDay": ["09:00"], "daysOfWeek": [1, 2, 3, 4, 5], "startDate": "2026-09-01T09:00:00", "timezone": "UTC" } }
customPeriodType is days, weeks, or months. Each day filter is read by
exactly one of them — daysOfWeek (0–6, Sunday is 0) by weeks, daysOfMonth
(1–31) by months — and ignored without error by the others. So "weekdays
at 09:00" is weeks with daysOfWeek, not days: days with daysOfWeek is
accepted and then runs every day, weekends included.
At most once a day. The sub-daily periodUnit values (minute,
fifteen-minutes, hourly) and more than one timesOfDay entry are rejected
as a validation_error. Do not promise a user anything more frequent.
At most 20 schedules per workspace. Creating past the cap is a
validation_error naming the limit. Deleting a schedule frees its slot;
pausing does not free one.
Before you create one
Read clay schedules list first. If one of the user's schedules already does roughly what this
one would, say so and ask whether they still want a second — duplicate names and overlapping
purposes are both allowed, so this is the user's call, not yours to skip.
Set timezone to the zone the user means. For a simple recurrence, write startDate
as local wall time without an offset, for example 2026-09-01T09:00:00 with
America/New_York. For a custom recurrence, write timesOfDay as local wall time in
timezone, never converted to UTC. For example, 9 AM in America/New_York is
"09:00" in timesOfDay.
Read the prompt for cadence and compare it with scheduleConfig — on create, and on any
update that touches the prompt. When they agree, remove the cadence sentence from the prompt
and proceed. When they disagree on any element — frequency, day, time, zone, or a date — ask the
user which one to keep before creating or updating anything, offering both as choices with the
instructions' cadence first, each spelled out ("Every Monday at 9:00 AM", "Every weekday at
9:00 AM") and each saying where it came from. Then put the chosen cadence in scheduleConfig
and take the cadence sentence out of the prompt, so the schedule says one thing.
Measuring the goal
Nothing about a goal is stored on the schedule or shown in the Clay UI. Progress lives in one place: each occurrence takes the reading the Goal section describes and states it in plain text in its final reply. That makes the Goal section a promise — write a measure into it only when the number will actually reflect the goal.
Choosing the measure
- Restate the goal as a measurable question. Decide the entity (people or companies)
and what "done" looks like as data, then match it to the
clay audiencescommand that answers it — none of these is a fallback for the others:- The goal is about record state — a field filled, a segment grown or drained
("enrich every founder", "200 accounts with a verified email") → a record count from
clay audiences records search-count, as a coverage ratio when that is what the goal means ("860 of 1,200 enriched"). - The goal is an outcome that happens — meetings booked, emails sent, calls made,
replies received ("book 25 meetings this quarter") →
clay audiences activities summaryover the relevant activity types and window. Check its--helpfor the accepted--activity-typesvalues. - The goal is about buying signals firing — job changes, funding news, intent →
clay audiences signals summary.
- The goal is about record state — a field filled, a segment grown or drained
("enrich every founder", "200 accounts with a verified email") → a record count from
- Confirm the data can answer it. For record-state goals:
clay audiences fields list --entity-type <t>— is there a field that will actually reflect the outcome, and does it get updated by the work this schedule does? For outcome goals: runclay audiences activities summaryonce over the target segment — a workspace with no activity data of that type cannot measure it. If nothing will be accurate, stop here: tell the user what you looked for and why it cannot be measured, then offer the closest alternative — a proxy field, a different measure, or an unmeasured goal. Never write in a measure you expect to read wrong or stay stale. - Build and validate the query. Compose the filter and run
clay audiences records search-count --filter '...'— this validates the AST and returns the count in one call. A zero or implausible count is a signal to revise the filter, not to use it. - Reuse or create the audience. Prefer a fitting saved audience (
clay audiences list); otherwiseclay audiences create. Re-check withsearch-count --audience-id. - Write the measure into the Goal section by id — the audience id, the filter, or the segment id plus activity or signal types and window — so a fresh occurrence can re-take exactly the same reading without re-deriving it.
- Tell the user how progress will be measured and what the number means, including the current reading, so an already-met or impossible goal surfaces immediately.
Taking the reading during a run
A scheduled occurrence takes the Goal's reading before doing work and lets it shape the occurrence: skip work the numbers show is already done, focus where they lag, and state the reading in the final reply next to what changed and what failed. Use the command the Goal names:
- A saved audience's count:
clay audiences records search-count --entity-type <people|companies> --audience-id <id>. - A coverage ratio:
search-countrejects--audience-idand--filtertogether, so fetch the audience's own filter withclay audiences get <audienceId>, AND it with the Goal's condition in oneGroupOp, and runsearch-count --filterwith that. The audience count alone is the denominator. - Activity volume: sum
activityCountacross the groups returned byclay audiences activities summary --segment-id <audienceId> --activity-types <types> --since <window start>. - Signal volume: the same through
clay audiences signals summary --segment-id ....
A reading you cannot take — the audience returns not_found because it was archived, a
lookup fails — is a fact to report, not a blocker: do the occurrence's work without it and
say so in the reply. Readings are current-state only. When the schedule needs run-over-run
deltas, record each reading in the occurrence's scheduled-task memory file (your
scheduled-run instructions describe it) so later occurrences can compare; without workspace
memory, report the current value without a comparison rather than guessing one.
Status
status on every schedule is its lifecycle — whether it will run again — and
is one of three values:
active— running on its cadence.paused— the user paused it, or its stored cadence stopped parsing.completed— the cadence has no further occurrence, its creator is no longer in the workspace, or the user retired it. Saving a cadence with a laterendDaterevives it.
All three can be requested on update. Requesting completed also sets the
cadence's endDate to the end of today in the schedule's timezone, which is what
the editor then shows as the end date. The system writes completed on its own
when it claims the final occurrence, leaving the cadence as it is.
currentTask is the schedule's unsettled task ({ id, status }, or null once
every task has settled). Its status is the run's progress, independent of the
schedule's: pending, queued or running while the occurrence executes, and
waiting_for_user when it is blocked on a question. A paused schedule can still
carry a running task, and a completed one its final run. nextRunAt is the next
unclaimed occurrence — it is not tied to either status. lastRunAt is the last occurrence the scheduler consumed, stamped even
when that occurrence was skipped without running, so a value there does not by
itself mean work happened.
Semantics worth knowing
- You only see your own.
listreturns the schedules the current user created; another user's schedule ids returnnot_found, indistinguishable from ids that never existed. - Updates are partial. Only the fields in
--inputchange. Setting"status": "paused"clearsnextRunAt; resuming or changingscheduleConfigrecomputes it from now; changing onlynameorpromptdoes not shift the next run. Pausing an already-completedschedule is avalidation_error— save a cadence with a laterendDateto revive it instead. - Edits apply from the next occurrence.
updateis accepted while a run is in flight, including a run of this very schedule. The occurrence already running carries its own copy of the prompt and its own slot, so an edit never changes what it is doing — it takes effect from the next occurrence on. This is what lets a scheduled run adjust the schedule that started it. - Delete removes the schedule's history with it. Every run the schedule
produced, and the GTM Agent tasks those runs created, are deleted along with
the schedule; nothing here undoes it. Confirm with the user before deleting a
schedule that has run (
hasRuntrue) — set"status": "completed"instead when the runs should stay readable. Delete is aconflictwhile a task is executing, since that agent is mid-turn; a task merely waiting on the user is deleted along with the schedule and its question is dropped. Not idempotent — a second delete of the same id isnot_found. - Runs are gated at fire time. An occurrence runs only if the GTM Agent is
still enabled for the workspace and the creator is still a workspace editor;
otherwise it is skipped, not queued. A creator who has left the workspace, or
whose account is gone, also retires the schedule: the occurrence is skipped
and the schedule moves to
completedrather than waiting for the next one. - Occurrences do not pile up. If runs were missed (downtime, a long pause), the schedule resumes with a single next occurrence rather than replaying the backlog.
Error codes
JSON on stdout, a typed error envelope on stderr. Exit codes: 0 ok, 1
conflict, 2 validation, 3 auth, 4 rate-limit, 5 network, 6 not-found.
2covers a malformed--input, a cadence that runs more than once a day or has no future occurrence, a non-UUIDscheduleId, pausing a completed schedule, and the 20-schedule cap.1(conflict) covers deleting a schedule whose task is executing, and — on any command — the moment between the scheduler reserving an occurrence and starting it. The second is two statements wide; retry rather than reporting it as a limitation.6onlistusually means the GTM Agent is not enabled for the workspace; onupdate/deleteit usually means the id belongs to another user's or a deleted schedule.
Current limitations
Everything below is not yet, not impossible — check clay schedules --help
before telling a user something cannot be done.
- No way to answer a
waiting_for_usertask. There is noclaycommand that opens, replies to, or stops the task incurrentTask; tell the user to answer it in the Clay UI. Editing and deleting the schedule still work, so this blocks only answering the question itself. - No run history command. Nothing lists the tasks a schedule started or
whether the last occurrence succeeded;
lastRunAtonlistis the only trace, and it does not distinguish a run from a skip. listcannot filter server-side. It is cursor-paginated (--limit,--cursor), but the 20-schedule cap means one page holds them all at the default limit, socursoris normally absent. Narrow bystatuswithjq.- No cross-user visibility. There is no way to list or manage schedules other workspace members created.


