T01 — A month of reviews to an action plan
Osteria Le Tre Lune is a family-run trattoria in Lucca. Every month Marta, the owner, gets forty-odd reviews on three websites in five languages — and has neither the time to read them all nor the languages to answer them. This workflow reads every review, and gives her back a one-page report to discuss with the staff and a reply ready to post for every guest, in that guest’s language.
The reference run, on the reviews of September — 5.95 credits:
| Delivered | What is in it |
|---|---|
| Report | one page: the month in one sentence, the stars, what guests say by theme, three actions for this week, a guest’s words in the original and in English |
| Replies | 40 rows: review, guest, source, stars, language, URGENT or —, the reply |
| The month as data | every review with its language, summary, complaints by theme and its group of stars |
| Complaints by theme | waiting 11, service 10, food 5, atmosphere 4, prices 2 |

Two of the forty replies — the two the workflow marked urgent:
R05 · fr · URGENT — Chère Claire, nous sommes sincèrement désolés que vos pâtes aient été cuites dans la même eau malgré votre signalement de maladie cœliaque, et que vous ayez été malade. Nous prenons cette situation très au sérieux. Merci d’écrire à ciao@letrelune.example afin que Marta puisse vous appeler personnellement. Marta, Osteria Le Tre Lune
R10 · en · URGENT — Dear Robert, thank you for your feedback, and please accept our sincere apology for the wine being added to your bill and for the lack of an apology when it was corrected. Please write to ciao@letrelune.example so Marta can call you personally. Marta, Osteria Le Tre Lune
The case
Section titled “The case”The reviews arrive as one spreadsheet, exported from the three websites:
review_id,date,source,rating,author,textR03,2026-09-03,TheFork,2,Sabine K.,"Das Essen war gut, aber wir haben am Samstag über eine Stunde auf den Hauptgang gewartet. …"R05,2026-09-05,Google,1,Claire D.,"J'avais précisé à la réservation que je suis cœliaque. On m'a servi des pâtes « sans gluten » … "R17,2026-09-15,Google,5,Hans M.,"Hier isst man wie bei einer italienischen Familie. Einfache Gerichte, perfekt gemacht. …"Forty reviews, average 3.975 stars: the food and the welcome are praised in every language; Saturday nights are slow; one guest with coeliac disease was made ill; one bill had a bottle too many; a booking was lost; the card machine failed. The reviews, the osteria and its guests are fictional, written for this example.
| Input | Who provides it | Example |
|---|---|---|
| Reviews of the month | an export from the review sites, or a sheet kept by hand | a CSV or Excel file uploaded as an asset |
| Restaurant name, period, link to all reviews | the owner | Osteria Le Tre Lune, September 2026 |
What it shows
Section titled “What it shows”- The model reads; the workflow counts. Each review is read by Text Generation into a JSON object — the language,
a 0 or 1 for each of five themes, whether it is urgent, a summary, a reply — and a schema checks it. From then on
no model touches a number: Calculate turns the stars into three groups (
if(rating >= 4, 1, 0)and its siblings), and after Generate JSON has collected the month it counts and averages them —avg(month[*].rating),sum(month[*].complaints.waiting). The figures on the page reach the template through their own field inputs (byline,step_1…step_3), written by Format as Text and Text Template: no model writes them, so none can round, invent or mistype them. The report writer is given the counts by theme only to decide what matters most, and is told not to repeat them. See Calculating and Structured answers. - A rule in a schema blocks a form, not an intent. The schema of the page refuses any digit in the model’s words
(
"pattern": "^[^0-9]*$"). On the first run the model wrote “one wine-list complaint” — a count in letters, which the pattern cannot see. The brief now also says never write how many guests or reviews say something, neither in figures nor in words, and the page says “some guests find the wine list costly”. - A quotation is verified, not trusted. The model picks a guest’s sentence for the page. A Replace Text looks for it, word for word, in the month’s reviews and reports how many times it found it; a Predicate and a Filter let the page’s words reach the template only if the count is at least one. A quotation the guests never wrote cannot be printed: the report is not made, and the illustration — the costly step — is not painted. See Deciding.
- Urgency is decided per review, and acted on. The reading marks a review urgent when a guest’s health or money is at stake or the guest asks to be contacted; the reply then apologises and asks the guest to write so the owner can call, and the report’s first action is about it. Two of forty were urgent: the coeliac guest and the wrong bill; the lost booking and the broken card machine were complaints, not emergencies.
- Every guest is answered in their language, with the right courtesy. The reply is written in the language of the review and, as the brief says, in the polite form where the language has one — Lei, Sie, vous, usted. On the first run without that line, the Italian reply said tu and the German ones mixed du and Sie.
- One template, used again. The page is the layout of the gallery’s visual summary,
copied with new headings — In one sentence, The month in stars, What guests say, This week. The AI’s words
go on
data_0; the figures, the title and the link go on their own fields and win over it.
The workflow
Section titled “The workflow”The figure above tells the workflow as stages; the exact graph is the workflow (as a diagram), node by node in the table.
| Node | Type | Why |
|---|---|---|
| Reviews of the month | input/data |
The spreadsheet, one review per row |
| Each review | enumerate/data_rows |
One iteration per review: id, source, stars, guest, text |
| Stars as text / The review as a brief | text/format, text/template |
The review in one short brief: who, where, how many stars, what they wrote |
| Read the review | ai/text_generation, JSON mode |
Language, complaints by theme (0 or 1), urgent, a summary in English, the reply in the guest’s language |
| Check the reading | utility/json_schema_validate, fail |
Flags only 0 or 1, a two-letter language, summary and reply within their lengths |
| Language, Complaints, Urgent, Summary, Reply | utility/extract |
The checked fields, typed |
| Happy, In between, Unhappy, Urgent as a number | utility/calculate |
The stars and the urgency as 0 or 1, ready to be added up |
| The month, review by review | aggregate/json |
Every review with its reading, as one JSON list |
| Urgent as a mark / Replies to send | text/format (URGENT / —), aggregate/csv |
One reply per guest |
| Reviews, Average stars, Happy guests, …, Complaints by theme | utility/calculate ×11 |
count, avg, sum over the month |
| … as text, Byline, Step 1–3, Complaints by theme | text/format (en-GB), text/template |
The figures as the page prints them |
| The month as text / Brief for the report | text/format json, text/template |
Everything the report writer reads: the counts by theme, every review |
| Write the page | ai/text_generation, JSON mode |
The sentence, four or five themes, three actions, a quotation and its translation, an illustration prompt |
| The words fit the page | utility/json_schema_validate, fail |
Lengths for the printed page; no digits in the AI’s words |
| The quotation / Is the quotation in the reviews? | utility/extract, text/replace |
The quotation counted in the month’s texts |
| Quotation found / Only a verified page goes to print | utility/predicate (≥ 1), utility/filter |
The page’s words pass only with a verified quotation |
| Illustration prompt / Paint the illustration | utility/extract, ai/text_to_image |
The picture of the page, from the report writer’s prompt — painted only once the quotation is verified |
| Render the report | design/template_render |
The AI’s words on data_0; title, byline, steps, link and picture on their fields |
| Outputs | output/pdf, output/image, output/csv, output/json ×3, output/text |
The report, the replies, the month, the check and the layout report |
Reading a run: Quotation found is the number of times the guest’s words were found in the reviews; the layout report says whether every text fitted its box.
Cost: about 0.02 credits to read each review, 0.16 for the page, 5 for the illustration — 5.95 for the month. Without the illustration the report costs under 1 credit.
Install it
Section titled “Install it”The example installs into your workspace with an API key, through the public API only — see Installing an example. It installs the document template too:
node madoo-install-example.mjs t01-reviews-action-plan --api <your Madoo API URL> --key <key.json> --run--run uploads the example reviews and processes them (about 6 credits). To process your own, run the workflow with
your spreadsheet on the input Reviews of the month: the columns it reads are review_id, source, rating,
author and text.
Change it
Section titled “Change it”- Your business: the two briefs name the osteria and its owner; rename them, and change the five themes — for a hotel rooms, breakfast, staff, cleanliness, location; for a shop products, delivery, returns, support, prices. A theme is a key in the reading’s schema and one Calculate.
- What counts as urgent: the reading’s brief says it — health, money, a request to be contacted. Add what matters to you: a safety issue, a legal threat, a regular customer.
- Where the replies go: the CSV can be pasted into the review sites, or sent to a system that posts them; a person should read the urgent ones before they go.
- No illustration: disconnect Paint the illustration and give the template a fixed picture of the place on its
illustrationfield; the run then costs under 1 credit. - Another language for the report: the report writer’s brief says Write in English; the headings are in the template copy.
manifest · workflow · the complete graph · the template · the reviews