Skip to content

16. Apps

Write a campaign generator: brainstorm three angles for a product, then write a tagline for each angle, all at the same time. Until now, your own code started each run and waited for it. An app is code that runs on Maitento itself, next to your agents. It starts interactions, reads their answers, loops, branches and runs things in parallel, with the logic in code you control rather than in a prompt.

You’ll learn

  • what an app is for: orchestrating agents with code you control, running on Maitento
  • how an app is laid out, where it starts (main), and how its parameters become its inputs
  • the system calls that start interactions, wait for them and read their answers, in parallel
  • how a finished app run reports what it did, and what went wrong

New concepts: apps · Cogniscript · system calls · return values and named outputs

Builds on: 3. Typed answers (the brainstorm answers in JSON that matches a schema)

Time: ~15 min · Cost: under $0.01 on DeepSeek (one app run makes four short runs) · Needs: nothing beyond Get started

Two interactions do the AI work, both with a copywriter agent:

  • brainstorm takes a product, and answers with three angles as JSON that matches json/ideas.json;
  • tagline takes a product and an idea, and writes one tagline.
maitento/interactions/one-shot/tagline.mtr
agent = {
name = "Copywriter"
agent = @ref("copywriter")
}
task = "Write one tagline of at most ten words for %%product%%, using this angle: %%idea%%. Reply with the tagline only."
inputs.prompts = [
{
name = "product"
required = true
},
{
name = "idea"
required = true
}
]

The app ties them together. It’s written in Cogniscript, a small language that looks like C, in .mtc files:

maitento/apps/taglines/taglines.mtc
// The taglines app: it asks for three campaign angles, then writes a tagline for each angle, all at once.
// An app starts at main. main's parameters are the app's inputs: this one has one, product.
string main(string product) {
// Stop early, with a clear message, when the input is missing.
if (product == "") {
throw("Tell me which product to write taglines for.");
}
PrintLine("Brainstorming angles for " + product);
// Start the brainstorm interaction and wait for it. The last three arguments are for memory,
// model overrides and a response schema; this app uses none of them.
jsonObject brainstormInputs = JsonObjectCreate();
brainstormInputs = JsonObjectSetString(brainstormInputs, "product", product);
guid brainstormId = InteractionStart("brainstorm", brainstormInputs, "[]", "[]", "");
jsonObject brainstorm = InteractionAwait(brainstormId);
if (JsonObjectGet(brainstorm, "status") != "Finished") {
throw("The brainstorm failed: " + JsonObjectGet(brainstorm, "failureDetails"));
}
// The brainstorm's answer is JSON that matches json/ideas.json.
jsonObject result = StringToJsonObject(JsonObjectGet(brainstorm, "output"));
jsonArray ideas = JsonObjectGet(result, "ideas");
// Start one tagline run per idea. They all run at the same time.
jsonArray runIds = JsonArrayCreate();
int ideaCount = JsonArrayCount(ideas);
for (int i = 0; i < ideaCount; i++) {
jsonObject inputs = JsonObjectCreate();
inputs = JsonObjectSetString(inputs, "product", product);
inputs = JsonObjectSetString(inputs, "idea", JsonArrayGetAtIndex(ideas, i));
guid runId = InteractionStart("tagline", inputs, "[]", "[]", "");
runIds = JsonArrayAdd(runIds, runId);
}
// Wait for every tagline run, then collect the answers.
jsonArray runs = InteractionAwaitAll(runIds);
jsonArray taglines = JsonArrayCreate();
int runCount = JsonArrayCount(runs);
for (int j = 0; j < runCount; j++) {
jsonObject run = JsonArrayGetAtIndex(runs, j);
if (JsonObjectGet(run, "status") == "Finished") {
string tagline = JsonObjectGet(run, "output");
PrintLine("- " + tagline);
taglines = JsonArrayAdd(taglines, tagline);
}
}
// A named output is what your code reads back from the finished run. return gives the run's return value.
AppStateSetOutputJsonArray("taglines", taglines);
return(JsonArrayCount(taglines) + " taglines");
}

How it fits together:

  • The folder is the app. apps/taglines/ holds the code, taglines.mtc, and an optional settings file, taglines.mtr, which here stops the app if it runs for more than five minutes. The folder’s name is the app’s name.
  • main is where it starts. Its parameters are the app’s inputs, so main(string product) makes an app with one input, product. Every input is required, and arrives as text.
  • System calls are the functions Maitento gives an app:
CallWhat it does
PrintLine(text)writes a line to the run’s console
InteractionStart(name, inputs, memory, modelOverrides, schema)starts an interaction run, and returns its id straight away. The last three can be empty: "[]", "[]", ""
InteractionAwait(id)waits for one run, and returns it as JSON: its status, output and failureDetails
InteractionAwaitAll(ids)waits for every run in a list
JsonObjectCreate, JsonObjectSetString, JsonObjectGet, StringToJsonObjectbuild and read JSON objects
JsonArrayCreate, JsonArrayAdd, JsonArrayCount, JsonArrayGetAtIndexbuild and read JSON lists
AppStateSetOutputJsonArray(name, value)publishes a named output, for your code to read back
  • return and throw end the run. return(...) gives the run’s return value. throw(...) stops it with an error. Both need brackets.
  • JSON helpers return a new value, so assign it back: inputs = JsonObjectSetString(inputs, "idea", idea);.

The tagline runs start one after another, but run at the same time: InteractionStart doesn’t wait, and InteractionAwaitAll collects them all.

Done when: the project validates. Run mt build ./maitento, or press Validate in the portal’s Build studio. A Cogniscript mistake is reported there, with its file and line, before anything is deployed.

import { Maitento } from "@maitento/sdk";
const mt = new Maitento();
await mt.deploy("../maitento");
// Run the taglines app. Its main(string product) parameter is its one input.
const product = process.argv[2] ?? "a reusable bamboo coffee cup";
const run = await mt.apps.run("taglines", { inputs: { product } });
// What the app printed, what it returned, and its named output.
for (const line of run.consoleOutputs) console.log(line.text.trimEnd());
console.log(`\nReturned: ${run.returnValue}`);
console.log(`Output "taglines": ${run.outputs.find((output) => output.name === "taglines")?.value}`);
Brainstorming angles for a reusable bamboo coffee cup
- Crafted bamboo, naturally beautiful—your upgrade, not your sacrifice.
- One cup. One year. Zero single-use waste.
- Make it yours. Sip your ritual. Share the movement.
Returned: 3 taglines
Output "taglines": ["Crafted bamboo, naturally beautiful—your upgrade, not your sacrifice.","One cup. One year. Zero single-use waste.","Make it yours. Sip your ritual. Share the movement."]

A finished app run carries everything the app reported:

On the runComes from
consoleOutputsPrintLine
returnValuereturn(...) in main
outputsAppStateSetOutput…, one per name
failureDetailsa throw, or an error the app didn’t catch

Open it in the portal’s Runs: the Console, Outputs and Inputs tabs show the same, and Children lists the four interaction runs it started.

Done when: you see three taglines and Returned: 3 taglines.

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"

Run the app with an empty product:

node taglines.ts ""
ProcessFailedError: App run 6422bf07-1913-4e6c-8a42-391b49d42f11 ended Error: Tell me which product to write taglines for.

The app’s first check threw, so the run ended with status Error, and its failureDetails hold the message from throw. apps.run raises that as an error (ProcessFailedError in TypeScript and Python, MaitentoProcessFailedException in C#). To get the failed run back instead, pass throwOnFailure: false in TypeScript, throw_on_failure=False in Python, or ThrowOnFailure = false in C#‘s RunOptions, and read status and failureDetails yourself.

An error the app doesn’t catch ends the run the same way, and its failureDetails name the system call that failed, then say why, as in InteractionStart failed: …. Without the check, the brainstorm would have been started with an empty product, and that error would end the run instead, less clearly. Check your inputs first.

  • The deploy fails with “Argument count mismatch for InteractionStart”. A system call takes every argument, even the ones you don’t need. Pass "[]", "[]", "" for those.
  • The run fails with “InteractionStart failed: Interaction ‘brainstorm’ was not found in namespace …”. The app named an interaction that isn’t in its namespace. Check the spelling, and that you deployed the whole project.
  • The run fails with “requires input for product - but value was empty”. An interaction the app started got an empty input. Check the inputs you build with JsonObjectSetString.
  • A tagline is missing. That tagline run didn’t finish, and the app skips runs whose status isn’t Finished. Open the app run’s Children to see why.

You built:

  • an app that brainstorms three angles, then writes a tagline for each, all at once;
  • a script that runs it and reads back its console, return value and named output;
  • an input check that stops the app with your own message.

The concepts:

  • App: code that runs on Maitento next to your agents. It’s deployed and versioned with them, and anyone can run it from the portal, the CLI, an automation or another app. It keeps going without you, and keeps one record of the whole job, with every child run linked.
  • Cogniscript: the small C-like language apps are written in, in .mtc files. An app starts at main, whose parameters are its inputs.
  • System calls: how an app reaches Maitento. InteractionStart doesn’t wait, so starting several runs and then InteractionAwaitAll runs them at the same time.

Challenge: add a judge. After the taglines are written, start one more interaction that picks the strongest, and make that the app’s return value. Keep taglines as it is, because tutorial 17 schedules it. The solution is in examples/16-apps/solution: a new interaction and a new app, deployed on top of the tutorial’s project.

Next: 17. On a schedule runs this app every morning.