Skip to content

17. On a schedule

Have Maitento run tutorial 16’s taglines app every morning at 07:00 UTC, with none of your code running. An automation says what to start and when. Maitento’s API calls it an automated action.

You’ll learn

  • how to say what an automation starts (an action) and when (a schedule, in UTC)
  • how to run it now, to test it, instead of waiting for the schedule
  • where to see what every run did, scheduled or not, and why one failed

New concepts: automations · cron schedules · the history

Builds on: 16. Apps (this tutorial schedules its taglines app)

Time: ~10 min · Cost: under $0.01 on DeepSeek per run, one run a day once it’s scheduled · Needs: nothing beyond Get started

An automation has two parts:

  • actions say what to start. Here, one action starts the taglines app with its product input. An action names an app version, so the script looks up the latest one;
  • schedules say when. A schedule is a cron expression in fields, and its times are UTC.
import { Maitento, type CreateAutomatedActionRequest } from "@maitento/sdk";
// This tutorial schedules the taglines app from tutorial 16, so deploy that project first.
const mt = new Maitento();
await mt.deploy("../../16-apps/maitento");
// An action says what to start: the latest version of the taglines app, with its input.
const app = await mt.apps.getByName("taglines");
const latest = Math.max(...app.versions.map((v) => v.version));
const request: CreateAutomatedActionRequest = {
name: "daily-taglines",
description: "Fresh taglines every morning",
actions: [{
name: "write-taglines",
config: { $type: "app", version: { id: app.id, version: latest }, inputs: [{ name: "product", value: "a reusable bamboo coffee cup" }] },
}],
};
// Create the automation, or point an existing one at the app's latest version.
const existing = await mt.automations.findByName("daily-taglines");
let automation = existing ? await mt.automations.update(existing.id, request) : await mt.automations.create(request);
// A schedule says when: here, every day at 07:00 UTC.
if (!automation.schedules.some((s) => s.name === "every-morning")) {
automation = await mt.automations.addSchedule(automation.id, { name: "every-morning", minute: "0", hour: "7" });
}
// The automation's next run is the soonest of its schedules' next occurrences.
console.log(`${automation.name} runs taglines version ${latest}. Next run: ${automation.nextScheduledRunAt?.toISOString()}`);
daily-taglines runs taglines version 2. Next run: 2026-10-27T07:00:00.000Z

The script is safe to run again. It updates the existing automation, so the action always names the app’s latest version, and adds the schedule only once.

A schedule’s fields are minute, hour, dayOfMonth, month and dayOfWeek, plus an optional second. A field you leave out means “every”:

You wantFields
Every day at 07:00 UTCminute: "0", hour: "7"
Weekdays at 09:30 UTCminute: "30", hour: "9", dayOfWeek: "1-5"
Every 15 minutesminute: "*/15"

In the portal, open Automations: daily-taglines is listed with its next run.

Done when: the script prints a next run at 07:00 UTC.

Don’t wait until tomorrow. Trigger it, and watch its history:

import { Maitento } from "@maitento/sdk";
const mt = new Maitento();
const automation = await mt.automations.findByName("daily-taglines");
if (!automation) throw new Error("Run schedule.ts first.");
// Run it now, instead of waiting for the schedule.
const { processId } = await mt.automations.trigger(automation.id);
// Every execution goes in the automation's history, newest first. Wait until this one has finished.
let [entry] = await mt.automations.history(automation.id, { limit: 1 });
for (let attempt = 1; attempt < 60 && !(entry?.processId === processId && entry.dateCompleted); attempt++) {
await new Promise((resolve) => setTimeout(resolve, 2000));
[entry] = await mt.automations.history(automation.id, { limit: 1 });
}
console.log(`${entry.triggerType} run ${entry.processId}: ${entry.outcome}${entry.errorMessage ? ` (${entry.errorMessage})` : ""}`);
// The run itself is an ordinary app run.
if (entry.outcome === "Success") {
const run = await mt.appRuns.get(processId);
for (const line of run.consoleOutputs) console.log(line.text.trimEnd());
}
Manual run 3c9c5e8a-f24a-44dd-89eb-aef2a803b114: Success
Brainstorming angles for a reusable bamboo coffee cup
- Bamboo by nature, crafted to last every day.
- Sip the change: one cup, endless refills, zero waste.
- Make it yours. Sip your ritual. Skip the waste.

A trigger starts the action straight away, as the schedule will. Every execution, by hand or by the schedule, is recorded in the automation’s history: what started it (Manual or Schedule), the run, and its outcome. That’s where to look for scheduled runs:

  • From code: automations.history, newest first, or mt action-history daily-taglines.
  • In the portal: Automations shows each automation’s next and last run, and a Failing filter. Open daily-taglines for its History.
  • In Runs: each execution is an ordinary app run, with its console, outputs and child runs.

Runs start as the person who created the automation, and use your tenant’s model keys.

Done when: the history’s newest entry is a Manual run with outcome Success, and you see three taglines.

This tutorial has only scripts:

On GitHub: examples/17-on-a-schedule · TypeScript · Python · C#

The project it deploys is tutorial 16’s:

On GitHub: examples/16-apps · TypeScript · Python · C#

All the files in this project (6)
model = @ref("deepseek.deepseek-v4-flash")
prompt = "You are a sharp, playful marketing copywriter."
summary = "Writes marketing copy"

In schedule, misspell the app’s input: change "product" to "prodcut". Run schedule:

ValidationError: PUT /automated-actions/by-id/9b1f0c2e-4d7a-4c55-8f63-2a0de8c4a1b7 failed with 400 (api:automations:app-input-missing): App 'taglines' version 2 needs a value for every input, and this action gives none for: product

Every input an app declares is required, so Maitento checks an app action’s inputs when you save the automation, and refuses this one. Without that check, the mistake would only show at 07:00, when the scheduled run failed with nobody watching. Python raises ValidationError too, and C# MaitentoValidationException, with the same code.

Some failures can only happen when a run starts, such as an app you deleted after creating the automation. Then trigger fails with api:automations:start-failed and says why, and a scheduled run that can’t start is in the history as Failed, with the reason. After you change an automation, trigger it once and check its history.

Change "prodcut" back to "product", and run schedule again.

  • It ran at the wrong time of day. Schedules are in UTC, not your local time. Work out the UTC hour, and remember that daylight saving moves your local time, but not UTC.
  • A scheduled run uses an old version of the app. An action names one app version, and keeps running it. After you change the app, run schedule again: it points the action at the latest version.
  • trigger fails with api:automations:start-failed. The action couldn’t start. The error says why, and so does its history entry.
  • Nothing ran at 07:00 UTC. Check that the schedule is switched on (isEnabled, or its switch in the portal) and that the history shows no failure.

You built:

  • an automation that runs the taglines app every morning at 07:00 UTC, always on its latest version;
  • a script that runs it now and reads the result from its history;
  • a broken input that Maitento refused when you saved the automation, not at 07:00.

The concepts:

  • Automation (an automated action in the API): actions say what to start, an app, interaction, capsule or code pipeline at a given version with its inputs; schedules say when.
  • Cron schedule: fields such as minute, hour and dayOfWeek. Always UTC.
  • Trigger and history: a trigger runs the actions now. The history records every execution, by hand or by schedule, with the run, its outcome and any error. Automations can also start when another run changes, with a message trigger.

Challenge: change the automation to run on weekdays only, at 09:30 UTC, instead of every day at 07:00. The solution is in examples/17-on-a-schedule/solution. To go back to every morning afterwards, remove the weekday schedule and run the tutorial’s schedule script again.

Next: 18. Sub-interactions has an agent start other agents.