MCP
Scheduling
Source: orchestration/mcp-gateway/extras.ts (ScheduleSchema, plungeaiSchedule, ownedJob), orchestration/mcp-gateway/tools.ts (scheduler ids in getResult / getWorkflowStatus), orchestration/mcp-gateway/server.ts (TOOL_DEFS), orchestration/scheduler/scheduler.ts (/jobs routes), orchestration/scheduler/SchedulerDO.ts (heartbeat, model pin, local bots, manual runs), orchestration/scheduler/utils/cronParser.ts, orchestration/scheduler/utils/d1Manager.ts, orchestration/scheduler/README.md.
plungeai_schedule creates and manages cron jobs on the platform scheduler. A job belongs to the user behind your key, and every run of a workflow, agent, query or mission_ref job lands in your normal execution history (heartbeats are the exception, 11.3), where you read it like any other result (Runs, results & follow-ups). The examples use https://mcp.plungeai.com/v1. https://api.plungeai.com/v1/mcp is the same server.
The tool is not marked read-only, but stats, list, get and runs only read. Full parameter table: 14.19.
11.1 How a schedule runs
The scheduler checks for due jobs continuously; the shortest cadence is one minute. A job id looks like job_<timestamp>_<random>.
Everything this tool creates, except heartbeats, runs as a workflow job. Agent and query jobs are wrapped into a saved workflow first, so every run goes through the standard engine pipeline and shows up in your history:
| You create | What is saved | What each run does |
|---|---|---|
job_type: "workflow", target = one of your workflow ids | the job, linked to that workflow | runs that workflow |
job_type: "agent", target = an agent id, parameters.prompt | a new workflow Scheduled: <name> with one task (that agent, that prompt), and the job | runs the wrapper workflow |
job_type: "query", target = {"agent": "<id>", "query": "<text>"} | the same kind of wrapper workflow | runs the wrapper workflow |
mission_ref = a pre-built agent card id | a wrapper workflow Scheduled: <name> with one harness task pointing at the card | runs the card, resolved live at fire time |
job_type: "heartbeat" | the job | checks a condition each tick and acts only when it is met (11.3) |
Wrapper workflows are ordinary saved workflows: they appear in plungeai_list_workflows and in Studio. Deleting a job does not delete its wrapper.
A workflow run is handed to a background run and the scheduler moves on; the run finishes on its own and the job's history is completed afterwards. Results can also be delivered to you (parameters.deliver, 11.3).
11.2 Actions
action | Needs | Answer |
|---|---|---|
stats | nothing | # Schedule stats with Jobs: N (a active, p paused) |
list | optional limit (default 50) | # Your scheduled jobs (N) and a table: Name, Type, Schedule, Status, Next run (epoch milliseconds), ID. Or No scheduled jobs. Create one with action "create" (name, job_type, target, schedule). |
get | job_id | # <name> and the whole job as JSON |
create | see 11.3 | Created scheduled job **<name>** (`<job_id>`) — <cron>. plus a note naming the wrapper workflow, if one was made |
update | job_id and the fields to change | Updated job `<job_id>`. |
pause | job_id | Job `<job_id>` is now paused. |
resume | job_id | Job `<job_id>` is now active. |
delete | job_id | Deleted scheduled job `<job_id>`. Its run history is preserved — action "runs" with this job id still shows past executions. |
run_now | job_id | Triggered job `<job_id>` — execution `<id>` (fetch the output with plungeai_get_result). |
runs | optional job_id, limit (default 20) | # Scheduled runs (N) and a table: Job, Status, Started, Execution ID, Error |
wake | job_id, optional note | Woke job `<job_id>` — it runs at <time>. The job's saved workflow runs once through the scheduler's runAgain, with note as its input; a pending wake is re-timed, never stacked |
webhook_mint, email_mint, hooks_mint | job_id; webhook: prompt_template, scheme, filters; email: allowed_from; hooks: url, events | the scheduler's answer as JSON (token, URL or address part, signing secret). A secret is in this answer once and never listed again |
webhook_list, email_list, hooks_list | job_id | the job's triggers of that kind as JSON (never a secret) |
webhook_delete, email_delete, hooks_delete | job_id, trigger_id | Deleted <kind> trigger `<id>` of job `<job_id>`. An id the job does not have is Trigger not found. |
webhook_rotate, email_rotate | job_id, trigger_id | mints a successor with the old trigger's settings, then revokes the old one (the same body as One API POST /v1/triggers/{webhook|email}/{token}/rotate): Rotated <kind> trigger `<id>` of job `<job_id>`: the old one is revoked. and the new trigger as JSON, with "rotated": "<old id>". The new secret is in this answer once. Mint comes first, so a failure in between leaves two live triggers, never none. Hooks have no rotate: delete and mint again |
stats and list read up to 1,000 of your jobs and leave deleted jobs out. In list, agent and query jobs show the type workflow, because they were wrapped.
11.3 Create a job
Every create needs name and schedule. The rest depends on the kind of job. The tool picks the path in this order: job_type: "heartbeat" first, then mission_ref (when set, job_type and target are not needed), then the plain job_type + target path.
schedule is a five-field cron expression: minute hour day-of-month month day-of-week. For a one-time run use schedule: "@once" with run_at, an ISO-8601 time with a UTC offset (for example 2026-10-06T09:00:00Z); without run_at the call answers run_at required and creates nothing. ends_at (ISO-8601, on create or update) stops a recurring job after that time. The webhook and email triggers belong to workflow jobs, and the hooks (outbound, signed run events) to any job you own; the same set is on the One API as /v1/schedules and /v1/triggers.
| Field | Values |
|---|---|
| minute | 0–59 |
| hour | 0–23 |
| day of month | 1–31 |
| month | 1–12 |
| day of week | 0–6, Sunday = 0 |
* is any value, , a list (1,15), - a range (1-5), / a step (*/15).
| Schedule | Cron |
|---|---|
| every 5 minutes | */5 * * * * |
| every hour | 0 * * * * |
| every day at 09:00 | 0 9 * * * |
| weekdays at 09:00 | 0 9 * * 1-5 |
| every Monday at 07:00 | 0 7 * * 1 |
| the 1st of every month at midnight | 0 0 1 * * |
@once fires through run_at: the job runs once at that time and is then done. For a run right now, use run_now.
timezone is an IANA zone name such as America/Los_Angeles or Europe/Berlin. The cron is read in that zone. Without it the cron is read in UTC. An invalid zone is refused, never silently treated as UTC.
description is optional free text.
parameters is an object of job extras:
| Key | Meaning |
|---|---|
prompt | the per-run goal or input. Required for agent jobs. For a workflow job it is the run's input (the scheduler reads initial_input, then input, then prompt). For a mission_ref job it is the goal, default Run your mission for today. |
inputs | named inputs for a workflow job |
deliver | where results go: an array of {channel, chat_id?, to?} with channel one of inapp, email, slack, telegram, whatsapp, discord, teams. A bare {channel:"slack"} sends you a direct message; a bare {channel:"email"} mails your registered address; any other email address must be a confirmed recipient. A bare string such as "slack" is accepted and stored as {channel:"slack"} |
Workflow jobs
job_type: "workflow" and target = the id of one of your saved workflows (from plungeai_list_workflows).
- The workflow must be in your account, or the answer is
No workflow `<id>` found in your account — pass a workflow id from plungeai_list_workflows, or build one with plungeai_build_workflow. - The scheduler refuses a workflow that names an agent id that does not exist:
Scheduler rejected the job: Cannot schedule — no such agent: <ids>. … - Model pin. When you create the job, the first
model:line in the workflow YAML is recorded, at any indentation (a task'smodel:counts). If that firstmodel:value changes later, each run is skipped instead of running on the new model. The run is still recorded ascompletedinruns, and the notice⚠️ Skipped this run: the model changed from <old> to <new> since you scheduled it. Re-save the schedule to accept the new model.goes to the job'sparameters.delivertargets. With nodeliverthe skip is silent. Theupdateaction does not re-record the model; create a new job to accept it. A workflow with nomodel:line anywhere is not pinned. - Local bots. A workflow of kind
botwhose YAML sayslocal: trueruns on your own computer. When your computer is offline at fire time, the run is skipped instead of failing, and a notice (⏭️ Skipped "<name>" — this bot runs on your computer, which was offline …) goes to the job'sparameters.delivertargets, if it has any.
Agent and query jobs
- Agent:
job_type: "agent",target= an active registry agent id, andparameters.prompt= what the agent should do each run. Without the prompt:Agent schedules need a prompt — pass parameters: { "prompt": "what <agent> should do each run" }. - Query:
job_type: "query",target= a JSON string{"agent": "<agent-id>", "query": "<what to run>"}. Both keys are required.
The agent is checked when you create the job: an unknown or inactive agent is refused before anything is saved in the scheduler (No such agent: … / Not permitted: … is not status:active …). Take agent ids from plungeai_list_agents (Discovery).
Pre-built agent cards (mission_ref)
Pass mission_ref with the card id, plus name and schedule. Each run executes the card as a one-task harness workflow with parameters.prompt as its goal. The card is resolved live at fire time, so edits to the card apply to future runs automatically.
Heartbeats
A heartbeat is a proactive check, such as "check my inbox for urgent email every 30 minutes". It needs job_type: "heartbeat", name, schedule, check_agent (or check_workflow) and condition_prompt; trigger_workflow, notify_channel and notify_chat_id are optional. The tool builds the job's target from these fields.
Each tick:
check_agentis askedcondition_promptand told to answer only YES or NO. Withcheck_workflowinstead, that saved workflow runs as you withcondition_promptas its input and its last task must answer YES or NO.- On NO, nothing else happens.
- On YES,
trigger_workflow(a workflow id) runs withcondition_promptas its input, if set. - Then a notification goes to
notify_channel/notify_chat_id, if both are set.
A heartbeat's check and its trigger_workflow run directly on the engine as system runs and create no execution in your history. run_now and runs show only the scheduler's own exec_… id, which the result tools cannot open, and the triggered workflow's output does not appear in plungeai_executions. Set notify_channel / notify_chat_id to see the outcome.
check_agent must be an active agent. check_workflow must be the id of a workflow you own, with no unknown agent in it. Studio's Gmail trigger is a heartbeat built this way: a Gmail search task, then a YES/NO judge. notify_channel is one of telegram, whatsapp, discord, slack, teams, email. WhatsApp notifications are partial: the WhatsApp adapter needs a phone-number id a heartbeat does not carry, so a WhatsApp heartbeat sends nothing today.
Create errors
All isError: true:
| Answer | Cause |
|---|---|
create requires: name, job_type (agent|query|workflow), target, schedule (cron). | a plain create with a field missing |
heartbeat create requires: name, schedule (cron), check_agent or check_workflow …, condition_prompt. … | a heartbeat with a field missing |
mission_ref create requires: name, schedule (cron). … | a mission_ref create with a field missing |
Scheduler rejected the job: Failed to parse cron expression: … | not a valid five-field cron |
Scheduler rejected the job: Invalid timezone: <zone> | not an IANA zone |
For job_type "query", target must be JSON: {"agent": "<agent-id>", "query": "<what to run>"}. / Query target must include both "agent" and "query". | a malformed query target |
The scheduler hit a temporary storage error while saving the job — please retry; it usually succeeds on the second attempt. | a transient storage error |
Heartbeat rejections read Scheduler rejected the heartbeat: ….
The wrapper is saved first. For agent, query and mission_ref jobs, the Scheduled: <name> workflow is saved before the scheduler validates the job. If the scheduler then rejects it (a bad cron, a bad zone), the wrapper stays in your workflows. Delete it with plungeai_workflow {action:"delete"} (Workflows).
Try asking:
- "Run the S&P 500 weekly performance analysis every Monday at 7am." Your client finds the workflow, then creates a workflow job with
0 7 * * 1(and your time zone). - "Every weekday at 9, have the news agent brief me on AI chip news and send it to Slack."
Create a workflow job. This saves a job that runs, and costs credits, on every tick (not verified here):
curl -s https://mcp.plungeai.com/v1 \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"run the S&P 500 weekly performance analysis every Monday at 7am","action":"create","name":"S&P 500 weekly","job_type":"workflow","target":"<workflow_id>","schedule":"0 7 * * 1","timezone":"America/New_York","parameters":{"deliver":[{"channel":"email"}]}}}}'Schedule one agent directly (the tool saves the wrapper workflow for you):
curl -s https://mcp.plungeai.com/v1 \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"every weekday at 9 brief me on AI chip news","action":"create","name":"AI chip news","job_type":"agent","target":"newsapi-get-articles","schedule":"0 9 * * 1-5","parameters":{"prompt":"Top AI chip news of the last 24 hours, five bullets with links.","deliver":["slack"]}}}}'11.4 Update, pause, resume, delete
job_id must be one of your jobs; anything else answers Job not found. Passing an execution id by mistake (exec_…) answers That looks like an execution id (exec_…), not a job id (job_…). ….
update changes only what you pass: name, description, schedule, timezone, target, parameters. parameters replaces the stored object, so send the whole object. Other keys (job_type, mission_ref, the heartbeat fields) are ignored on update.
- A workflow job runs the workflow it was linked to at create. A new
targetdoes not change that link. To schedule a different workflow, create a new job and delete the old one. - A rejected update answers
Scheduler update failed (HTTP <status>).without the reason. Check the cron and the zone: an invalid cron comes back as HTTP 500, an invalid zone as HTTP 400.
pause and resume set the job to paused or active.
delete is soft. The job stops and is marked deleted; its run history stays readable with runs and the job id. A deleted job refuses update, pause and resume (This job is deleted — it cannot be modified. …) and run_now (This job is deleted — it cannot be run. …). Deleting it again answers Job `<job_id>` is already deleted. Deleted jobs drop out of list and stats. The wrapper workflow of an agent, query or mission_ref job is not deleted.
Busy storage. When the scheduler's storage is briefly throttled, the job lookup is retried once. If it still fails, the answer is The scheduler’s storage is briefly busy (D1 throttling) — the job most likely still exists. Retry in a few seconds., never "Job not found".
Try asking: "What do I have scheduled?", "Pause the S&P 500 job.", "Move it to 8am.", "Delete that schedule."
Pause a job. This changes its state (not verified here):
curl -s https://mcp.plungeai.com/v1 \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"pause the S&P 500 job","action":"pause","job_id":"<job_id>"}}}'11.5 Run now and run history
run_now runs a job immediately, outside its cadence, and reports the scheduler's real outcome, not just "triggered".
- Success:
Triggered job `<job_id>` — execution `<id>` (fetch the output with plungeai_get_result).A workflow run is handed to a background run, so this answer comes back once the run has started, not when it has finished. Pollplungeai_get_workflow_statuswith that id, then read it withplungeai_get_result. - When the scheduler has only its own run id:
Triggered job `<job_id>` — scheduler run `exec_…` (see action "runs"). - Failure:
Run failed (scheduler run `exec_…`): <reason>(isError: true).
runs lists run history:
- with
job_id: that job's runs, including the runs of a deleted job; - without
job_id: the runs of your current jobs. Runs of deleted jobs are left out; ask for them by job id.
limit defaults to 20 (1–100). The columns are Job, Status, Started, Execution ID and Error (first 120 characters). The Execution ID is the id plungeai_get_result and plungeai_get_workflow_status accept: the platform execution id, or the engine's exec-… id. Heartbeat runs, and only very old runs of other jobs, fall back to the scheduler's own exec_… id, which those tools redirect back here. No executions for your jobs yet. means there are none.
Try asking: "Run it now.", "Show me the run history of that job."
Run history (read-only):
curl -s https://mcp.plungeai.com/v1 \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"show me the run history of that job","action":"runs","job_id":"<job_id>","limit":10}}}'Your schedule at a glance (read-only):
curl -s https://mcp.plungeai.com/v1 \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"what do I have scheduled?","action":"stats"}}}'Run a job now. This runs the job and costs credits (not verified here):
curl -s https://mcp.plungeai.com/v1 \
-H "Authorization: Bearer $PLUNGE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"plungeai_schedule","arguments":{"user_request":"run it now","action":"run_now","job_id":"<job_id>"}}}'