Agents APIAutomation
Schedules
Start a session at set times, such as every weekday at 9:30, and get its results without being there.
A schedule is the body of a session create plus a time, written as a cron expression. At each time, LatentStack starts an ordinary session from it, as you. Everything a session does works the same way: limits, network, files, outputs, patches, usage and webhooks.
Create a schedule
const schedule = await client.agents.schedules.create({
name: "standup video",
cron: "30 9 * * 1-5", // 9:30, Monday to Friday
timezone: "Asia/Kolkata",
session: {
agent: { model: "anthropic/claude-sonnet-5.5" },
environment: { type: "repos", repos: ["acme/demo"] },
input:
"Make a 30-second video for today's standup that sums up the commits of the last day. " +
"Save it as out/standup.mp4.",
limits: { max_cost_usd: 2 },
},
outputs: ["out/standup.mp4"],
webhook: {
url: "https://example.com/hooks/latentstack",
events: ["run.completed", "run.failed", "session.failed", "schedule.skipped"],
},
})
console.log(schedule.id, "next run:", schedule.next_run_at)
console.log("webhook secret:", schedule.webhook?.secret) // shown only now: give it to your receiverEach weekday at 9:30 in India, a session clones acme/demo, makes the video and stops. The video is kept as an output, and the webhook's run.completed carries a link to download it.
cronstringrequired30 9 * * 1-5 is 9:30, Monday to Friday. Read in timezone.timezonestringoptionaldefault UTCAsia/Kolkata or Europe/London.sessionobjectrequiredagent, environment, input, limits, metadata, files, outputs, end_after_run. Not webhook: that goes on the schedule.outputslist of pathsoptionalsession.outputs. Give it in one place or the other, not both.webhookobjectoptional{ url, events?, secret? }. Hears every run. See Webhook.namestringoptionalconcurrency"skip" | "allow"optionaldefault skipenabledbooleanoptionaldefault true- The session settings are checked when you save the schedule, as a create would check them: the model on your tier, the permission rules, and GitHub access to the repositories. A mistake fails now, not at 9:30.
- The answer is the schedule, with
next_run_at, the next time it will run, in UTC. - A schedule is yours alone, in the organization you made it in, like a session.
Each run
- At each time, LatentStack creates a session from
session, as the person who made the schedule. Your API key, your membership of the organization and your tier are checked again first. - The session's
schedule_idsays which schedule started it. So does thesessionblock on its webhook events. - It ends after its run:
end_after_runistrueunlesssessionsaysfalse. Its patch is saved, its outputs copied and its webhook sent, then it stops. See End after the run. - One time starts one session, even if LatentStack restarts while starting it.
- Its usage counts like that of any session you create, against your tier.
Files
Put files in session. They are kept with the schedule and put in each run's workspace before its first message. The schedule lists them as files: [{ path, size }], without their content.
Outputs
Each run's outputs belong to its own session. Get them from the run.completed webhook, or list them with the session id from the run ledger.
Webhook
webhookon the schedule hears every session the schedule starts, and the schedule'sschedule.skippedevents.eventsdefaults to["*"].- Its
secretis in the answer that made it, and only there, unless you passed your own. - Your registered endpoints hear the runs too.
When a run is skipped
A time that doesn't start a session is recorded as skipped in the run ledger, and a schedule.skipped event is sent. The schedule stays on, and the next time runs as usual.
| reason | Why |
|---|---|
| missed | LatentStack wasn't running at that time and came back more than an hour later. That time is skipped, not run late, and the schedule carries on from the next time to come. Back within the hour, it runs late, once. |
| previous_running | concurrency is skip, and the last run's session is still starting, running or waiting for an approval. |
| unavailable | A session couldn't be started for a whole hour: no capacity, your organization at its session limit, or GitHub out of reach. Until then the same time is tried again every minute. |
| key_revoked · key_expired | The API key you made the schedule with has been revoked or has expired. |
| not_a_member · user_not_found | You are no longer a member of the organization, or your account is gone. |
| project_access_denied | You are no longer in the team your tier came from. |
| model_not_allowed · no_tier_assigned | The model is no longer on your tier, or you have no tier in the organization. |
| another error type | The create failed, for example repo_not_accessible. message says why. |
{
"id": "evt_8c27d1e04b9a6f3e5d2c1b0a",
"object": "event",
"type": "schedule.skipped",
"api_version": "v1",
"created_at": "2026-10-15T04:00:04.212000+00:00",
"sequence": null,
"session": null,
"data": {
"schedule_id": "sch_1a2b3c4d5e6f7a8b9c0d1e2f",
"due_at": "2026-10-15T04:00:00+00:00",
"trigger": "clock",
"reason": "previous_running",
"message": "The previous run (ags_4843742656c9cbc90685c6d2) was still going."
}
}key_revoked. Create the schedule again with your new key, and delete the old one.Concurrency
| concurrency | When a time comes while the last run is still going |
|---|---|
| skip | The default. The time is skipped with previous_running. |
| allow | Another session starts anyway. |
A run is still going while its session is starting, running or waiting for an approval. A session that is idle, paused or stopped doesn't hold the next run back.
The run ledger
Each run gets one row, and so does each skipped time. Rows are newest first.
const { data } = await client.agents.schedules.runs(schedule.id, { limit: 10 })
for (const run of data) console.log(run.due_at, run.outcome, run.reason ?? run.session_id, run.cost_usd)| Field | Meaning |
|---|---|
| due_at | The time it was for. For a run you started by hand, when you asked. |
| trigger | clock, or manual for a run you started with run(). |
| session_id | The session it started, or null when skipped. |
| outcome | started while the session works on its run, then completed, failed, or stopped when the session stopped before its run ended (for example at a spend limit). skipped when no session was started. |
| reason · message | Why it was skipped. See When a run is skipped. |
| error · stop_reason | Why the run failed, or why its session stopped. |
| cost_usd | What the session had spent when its run ended. |
| started_at · ended_at | When the session was started, and when the run ended or was skipped. |
The schedule's own last_run_at, last_session_id and last_outcome sum up the latest run.
Run now
const run = await client.agents.schedules.run(schedule.id)
console.log(run.session_id)- Starts a session right away, outside the timetable. The next booked time doesn't change.
- The answer is the ledger row, with
trigger: "manual"and the newsession_id. - With
concurrency: "skip", it fails withconflictwhile the last run is still going. - When a session can't be started right now (no capacity, or your organization at its session limit), it fails with
runner_unavailableand the message says why. Try again shortly. - Any other reason a run would be skipped is returned as the error, such as
key_revoked.
Pause, change and delete
const schedules = client.agents.schedules
await schedules.update(schedule.id, { enabled: false }) // pause
await schedules.update(schedule.id, { enabled: true, cron: "0 10 * * 1-5" }) // resume, at 10:00 from now on
await schedules.delete(schedule.id)- Pause with
enabled: false.next_run_atbecomesnull. Turned on again, the next time counts from now: times that passed while it was paused don't run. - Change any field; the others keep their values. A new
cronortimezonebooks the next time from now. A newsessionreplaces the settings and their files, and is checked again. - A new
webhookreplaces the old one, whose waiting deliveries are marked failed.webhook: null(webhook=Nonein Python) removes it. - Delete stops all further runs. Sessions it already started keep going, and its webhook stays for 7 days so they can still report.
Over HTTP
| Request | Does |
|---|---|
| POST /v1/agents/schedules | Creates a schedule. Answers 201, with the webhook's secret when one was made. |
| GET /v1/agents/schedules?limit=&after= | Lists your schedules, newest first. |
| GET /v1/agents/schedules/{id} | Reads one. |
| PATCH /v1/agents/schedules/{id} | Changes the fields you send. |
| DELETE /v1/agents/schedules/{id} | Deletes it. Answers 204. |
| POST /v1/agents/schedules/{id}/run | Starts a run now. Answers 201 with the ledger row. |
| GET /v1/agents/schedules/{id}/runs?limit=&after= | The run ledger, newest first. |
On POST, session.files is an object of path to base64 content, as on session create. Lists hold 20 items by default and up to 100; pass next_after as after for the next page.