Skip to content

4. Runs

Every time an interaction runs, Maitento keeps a record of it: a run. In tutorials 1 to 3, run() started one and waited for its answer in a single call. Here you take that apart. You’ll start a run and let your script end while the agent works, check on it from another script, list your recent runs, wait for one yourself, and stop one half-way. You need this as soon as an answer takes longer than anyone wants to wait, or your code has other work to do in the meantime.

You’ll learn

  • starting a run without waiting for it
  • reading a run back by its id, and what its status tells you
  • listing your runs a page at a time
  • waiting for a run yourself, and what run() does for you
  • cancelling a run

New concepts: runs and their statuses · Builds on: 1. Hello, agent · Time: about 15 minutes · Cost: under $0.01 on DeepSeek · Needs: nothing beyond tutorial 1’s setup

In the API and the SDKs a run is called a process: you’ll see InteractionProcess and /interactions/processes/…. It’s the same thing.

An agent that explains things simply:

maitento/agents/explainer.mtr
model = @ref("deepseek.deepseek-v4-flash")
prompt = "You explain things clearly and simply, to a curious twelve-year-old."
summary = "Explains a topic in plain words"

And an interaction that asks for about 400 words. That takes a few seconds to write, so there’s time to look at the run while it works:

maitento/interactions/one-shot/explain.mtr
agent = {
name = "Explainer"
agent = @ref("explainer")
}
// Long enough to take a few seconds, so there is time to look at the run while it works.
task = "Explain %%topic%% in about 400 words."
inputs.prompts = [
{
name = "topic"
description = "What to explain"
required = true
}
]

Run every script from its language folder, as in tutorial 1: examples/04-runs/typescript (after npm install), examples/04-runs/python (after pip install -r requirements.txt) or examples/04-runs/csharp.

start() asks Maitento for a run and returns it straight away, in status New. The agent hasn’t begun yet, and your script doesn’t wait for it to: once Maitento has the run, it carries on whether your code is still there or not.

import { Maitento } from "@maitento/sdk";
const mt = new Maitento();
await mt.deploy("../maitento");
// start() returns as soon as Maitento has the run. It doesn't wait for the answer.
const run = await mt.interactions.start("explain", { inputs: { topic: "why the sky is blue" } });
console.log(`Started run ${run.id}: ${run.status}`);
console.log(`Check on it with: node status.ts ${run.id}`);

In C#, starting without waiting is InteractionProcesses.StartAsync, and it takes the namespace: mt.DefaultNamespace is the one in MAITENTO_NAMESPACE.

Started run 96187005-005d-4d06-83ec-fa9d767414bb: New
Check on it with: node status.ts 96187005-005d-4d06-83ec-fa9d767414bb

Done when: you see a run id and New, and the script has ended. Copy the id for the next step.

Pass the id to status.ts. It reads the run as it is right now. Once the run has finished, it also reads the run’s summary for the tokens it used:

import { Maitento } from "@maitento/sdk";
const id = process.argv[2];
if (!id) throw new Error("Pass the id of a run: node status.ts <run id>");
const mt = new Maitento();
// The run as it is right now.
const run = await mt.interactionRuns.get(id);
console.log(`Run ${run.id}: ${run.status}`);
if (run.status === "Finished") {
// runs.get() gives a short summary of any kind of run, with the tokens it used.
const summary = await mt.runs.get(id);
const seconds = (run.executionTime / 1000).toFixed(1);
console.log(`Worked for ${seconds}s, using ${summary.tokens?.input} tokens in and ${summary.tokens?.output} out.`);
// The start of the answer.
console.log(`${run.output?.slice(0, 160)}…`);
} else if (run.status === "Error" || run.status === "Killed") {
console.log(`It stopped: ${run.failureDetails}`);
}

Run it straight away, then again a few seconds later:

$ node status.ts 96187005-005d-4d06-83ec-fa9d767414bb
Run 96187005-005d-4d06-83ec-fa9d767414bb: Running
$ node status.ts 96187005-005d-4d06-83ec-fa9d767414bb
Run 96187005-005d-4d06-83ec-fa9d767414bb: Finished
Worked for 3.8s, using 256 tokens in and 391 out.
The sky appears blue because of a process called Rayleigh scattering. Sunlight looks white, but it is actually a mixture of all the colours of the rainbow. When…

Done when: the second time says Finished, with how long it worked and the tokens it used.

Two calls read a run, and they answer different questions:

  • interactionRuns.get(id) returns the whole interaction run: its status, its output once it has finished, failureDetails if it failed, its inputs and the conversation.
  • runs.get(id) returns a short summary of a run of any kind (interaction, app, capsule or code pipeline): its status, timings, token totals, and its state, one of Active, Waiting or Terminal. You’ll use the summaries in the next step.

The CLI has no command that reads one run by its id: mt interaction-history explain lists the interaction’s runs with their statuses.

Maitento lists runs newest first, a page at a time. This lists the runs in your namespace, five to a page, and then the page after:

import { Maitento, type ProcessSummary } from "@maitento/sdk";
const mt = new Maitento();
// One line per run: when it started, what it ran, and how it is doing.
const show = (run: ProcessSummary) =>
console.log(`${run.dateCreated.toISOString().slice(0, 19)} ${run.definitionName} ${run.status}`);
// Runs of every kind, newest first, a page at a time. Here: the runs in your namespace, five to a page.
const page = await mt.runs.list({ namespace: mt.namespace, limit: 5 });
page.items.forEach(show);
// The next page starts where this one ended. nextPage() is null after the last one.
const next = await page.nextPage();
if (next) {
console.log("--- next page");
next.items.forEach(show);
}
2026-10-26T19:08:03 explain Finished
2026-10-26T19:07:29 weather Finished
2026-10-26T19:05:55 explain Finished
2026-10-26T19:05:55 explain Finished
2026-10-26T19:05:55 explain Finished
--- next page
2026-10-26T19:05:49 explain Killed
2026-10-26T19:05:49 hello Finished
2026-10-26T19:05:41 explain Finished
2026-10-26T19:05:16 explain Finished
2026-10-26T19:05:02 hello Finished

Done when: your runs from steps 2 and 3 are near the top. Runs of other tutorials in the namespace are listed too.

Good to know:

  • Every page ends with a cursor (nextCursor), or none on the last page. In TypeScript and Python, for await (const run of mt.runs.list(…)) and for run in mt.runs.list(…) walk every page for you. In C#, pass NextCursor back as Cursor.
  • limit is 1 to 200, and 50 if you don’t say.
  • Filters narrow the list, and every one you give must match: state (Active, Waiting or Terminal), types (Interaction, App, Capsule, CodePipeline), statuses, ids (up to 50 runs at once), definitionId, createdAfter and createdBefore.

wait.ts starts a run and reads it back once a second until it ends, saying whenever its status changes:

import { isTerminalStatus, Maitento } from "@maitento/sdk";
const mt = new Maitento();
await mt.deploy("../maitento");
const started = Date.now();
let run = await mt.interactions.start("explain", { inputs: { topic: "how rainbows form" } });
console.log(`Started run ${run.id}`);
// Read the run once a second until it ends, and say whenever its status changes.
let status = "";
while (true) {
if (run.status !== status) {
status = run.status;
console.log(`${((Date.now() - started) / 1000).toFixed(1)}s ${status}`);
}
if (isTerminalStatus(run.status)) break;
await new Promise((resolve) => setTimeout(resolve, 1000));
run = await mt.interactionRuns.get(run.id);
}
// A finished run has its answer in output; one that failed or was killed says why in failureDetails.
console.log(run.status === "Finished" ? `${run.output?.slice(0, 160)}…` : run.failureDetails);
Started run 1a0a4c11-4272-4d10-848b-358cedd07396
0.1s New
1.1s Running
4.2s Finished
Rainbows form because sunlight and raindrops work together like a giant prism in the sky. Here is the step-by-step story.
…

Done when: you see the run go from New to Running to Finished, then the start of the answer.

This loop is what run() did for you in tutorials 1 to 3, with two more things:

  • It listens as well as asks. When an event connection is open (tutorial 5), run() hears about each change the moment it happens, instead of up to a second later.
  • It treats a failed run as an error. A run that ends Error or Killed makes run() throw ProcessFailedError (MaitentoProcessFailedException in C#). To get the run back instead, pass throwOnFailure: false (Python: throw_on_failure=False, C#: ThrowOnFailure = false).

Asking every second is fine for one run in a script. To keep track of many runs, have Maitento tell you when they change, with live events (tutorial 12) or webhooks (tutorial 11).

A run’s status says what it’s doing, and every status is in one of three groups:

An interaction run's statuses in three groups. Active, where you wait: New, then Running. Waiting, where a person, an approval or child runs must act before the run resumes: HumanInteractionRequired, PendingApproval, AwaitingChildren and HumanInteractionComplete. Final, where you read output or failureDetails: Finished, Error (failed or timed out) or Killed (stopped by someone). A run ends from Running or from Waiting.

  • Active: New (waiting for a worker, usually for a second or two), then Running.
  • Waiting: the run is paused until someone acts: a person replies (tutorial 7), approves a tool call (tutorial 10), or the runs it started finish (tutorial 18).
  • Final: Finished (the answer is in output), Error (the reason is in failureDetails) or Killed. A final status never changes again.

A summary’s state names the group (Active, Waiting or Terminal) for a run of any kind, so one filter works for interactions, apps, capsules and code pipelines alike. Each status is described in Interactions.

Start a run, let it get going, then kill it:

import { isTerminalStatus, Maitento } from "@maitento/sdk";
const mt = new Maitento();
await mt.deploy("../maitento");
const run = await mt.interactions.start("explain", { inputs: { topic: "how volcanoes work" } });
console.log(`Started run ${run.id}`);
// Let it get going, then stop it.
await new Promise((resolve) => setTimeout(resolve, 2000));
await mt.runs.kill(run.id, { type: "Interaction" });
console.log("Asked Maitento to kill it");
// A kill takes a moment: read the run back until it has stopped.
let stopped = await mt.interactionRuns.get(run.id);
while (!isTerminalStatus(stopped.status)) {
await new Promise((resolve) => setTimeout(resolve, 500));
stopped = await mt.interactionRuns.get(run.id);
}
console.log(`Run ${stopped.id}: ${stopped.status}`);
console.log(`Output: ${stopped.output ?? "none"}`);
console.log(`Why: ${stopped.failureDetails}`);
Started run 7f448901-9b9c-4d63-b153-2657b3b16333
Asked Maitento to kill it
Run 7f448901-9b9c-4d63-b153-2657b3b16333: Killed
Output: none
Why: Interaction killed by parent process

Done when: the run ends Killed, with no output.

  • A kill is final. The run stops where it is, keeps no answer and can’t be resumed. Tokens it had already used still count.
  • Only a live run can be killed. Killing one that has already ended fails with a ValidationError saying Process is not running.
  • The kill names the kind of run (Interaction, App, Capsule or CodePipeline), because runs.kill works for all of them.
  • run() and a killed run: if a run you are waiting on with run() is killed, run() throws ProcessFailedError. Giving up on waiting is different: a timeout or an abort signal stops run() following the run, but the run carries on unless you pass killOnAbort: true (Python: kill_on_abort=True, C#: KillOnCancel = true).

Open Runs in the portal. It lists every run with its status, when it started, how long it ran and the tokens it used, and updates live. Filter by kind, by Active, Waiting or Ended, by namespace or by status, and open a run to read its conversation. Go to run opens one by its id.

The Runs page: every run with its status, start time, runtime, tokens and namespace

  • An error saying the call needs a namespace: set MAITENTO_NAMESPACE to tutorials, as in Get started.
  • The run ends Error, and its failure details end with [api:model-keys:none-for-provider]: the tenant has no DeepSeek key. Add one (Model keys), or change model in explainer.mtr to a model you have a key for.
  • NotFoundError (a 404) from status.ts: the id is wrong, or the run is older than your tenant keeps runs: finished runs are deleted after two days unless your tenant says otherwise.
  • ValidationError: Process is not running from cancel.ts: the run finished before the kill reached it. Kill it sooner, or ask for a longer answer.
  • Cannot find module '@maitento/sdk' or No module named 'maitento': run the script from its language folder, after npm install or pip install -r requirements.txt there.

What you built:

  • start starts a run and ends, while the run carries on in Maitento.
  • status reads a run back by its id; list pages through your runs.
  • wait follows a run to the end by asking, the way run() does when nothing tells it sooner.
  • cancel kills a run half-way.

The concepts:

  • A run (a process, in the API) is Maitento’s record of one start of an interaction, app, capsule or code pipeline. It exists whether or not anyone waits for it.
  • A status is Active, Waiting or Final; a summary’s state names the group for any kind of run.
  • Listing runs is paged with a cursor, and filters by state, kind, status, id or date.

Challenge: start three runs at once, then wait until all three have ended, reading all three with one call a second. Hint: runs.list() takes ids, and a summary’s state is Terminal once a run has ended. A solution in TypeScript, Python and C# is in examples/04-runs/solution:

Started 3 runs
1 of 3 ended
2 of 3 ended
3 of 3 ended
why the sky is blue: Finished, 1946 characters
how bees make honey: Finished, 2034 characters
what a black hole is: Finished, 2071 characters

All the files of this tutorial’s project:

On GitHub: examples/04-runs · TypeScript · Python · C#

All the files in this project (2)
model = @ref("deepseek.deepseek-v4-flash")
prompt = "You explain things clearly and simply, to a curious twelve-year-old."
summary = "Explains a topic in plain words"

Next: 5. Watch it think follows a run live and shows the reply as it’s written.