$ cat workspace-template.yaml
Traduzione PDF di una Cartella
// Traduce tutti i file di una cartella Drive in un'altra cartella e mantiene le sottocartelle: sorgente/a/b/c/d.pdf diventa destinazione/a/b/c/d.<lingua>.pdf e d.<lingua>.bilingual.pdf, con ogni pagina originale accanto alla sua traduzione. Ogni file conserva l'impaginazione e passa i controlli del Traduttore PDF (nessun testo lasciato non tradotto, nessun numero cambiato); Word, PowerPoint e testo vengono tradotti dal PDF che Drive ne ha fatto. Prima di tradurre l'agente mostra l'elenco dei file e chiede conferma, poi crea sul canvas una sezione per ogni file e li traduce tutti con un solo avvio, in parallelo: un file non riuscito ferma solo la sua sezione. Alla fine riporta l'esito di ogni file. Translates every file of a Drive folder into another folder and keeps the sub-folders: source/a/b/c/d.pdf becomes destination/a/b/c/d.<lang>.pdf and d.<lang>.bilingual.pdf, with each original page beside its translation. Each file keeps its layout and passes the PDF Translator's checks (no text left untranslated, no number changed); Word, PowerPoint and text files are translated from the PDF Drive made of them. Before translating, the agent shows the file list and asks to confirm, then builds one section per file on the canvas and translates them all in one run, in parallel: a file that fails stops only its own section. At the end it reports every file's outcome.
// Canvas Preview
// Instruction
Traduzione PDF di una Cartella
Template ID: assigned by each marketplace when the template is published (source: fixture apps/marketplace/fixtures/workspace-templates/pdf-folder-translator.workspace.json, built by scripts/pdf-folder-translator/build_template.py).
Translates every file of a Drive source folder into a Drive destination folder and keeps each file's sub-folder: source/a/b/c/d.pdf becomes destination/a/b/c/d.<lang>.pdf (the translation) and destination/a/b/c/d.<lang>.bilingual.pdf (each original page beside its translation). Every file gets its OWN section on the canvas, cloned from the "File #x" prototype by ONE expandPrototype call, and ONE run of "Esito Cartella" translates and saves them all, in parallel.
You MUST complete all 5 phases in order: Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5. Do NOT skip any phase.
When this instruction reaches you as a section index, load each phase's section before entering it, starting with loadInstructionSection({ sectionId: "phase-1-setup" }). NEVER set llmModelId on any node: leave it empty.
Overview
A user has a folder of documents (supplier datasheets, declarations, a technical file) and wants all of it in another language, in a second folder laid out exactly like the first, so a reviewer finds each translation where the original is. Each file goes through the PDF Translator: it keeps the page layout, checks that every passage was translated and no number changed, and returns a translated PDF and, by default, a bilingual PDF. Word, PowerPoint and text files are translated from the PDF Drive made of them, so they also come back as PDFs. Spreadsheets, images, audio, video and scanned PDFs are not translated; they are listed in the report.
The canvas is the execution report: every file has its own section, named after the file, whose nodes show that file's run (its translator report, the saved names, or the error). One run translates the whole folder; a file that fails stops only its own section, and every other file is still translated and saved.
Every translation is a paid model run and every save writes to Drive, so nothing runs before the user confirms the file list (step 8).
Workflow Chain
The saved names come from "Traduttore PDF": d.pdf becomes d.<lang>.pdf and d.<lang>.bilingual.pdf. Each clone has its own Save File nodes, so every file keeps its own Drive file, and running a file again replaces it.
Node Reference
Canvas labels are Italian; match them exactly with nodeFinder. Port lists and defaults are in the schema reference in <current_workspace>. Inside the clones the node labels repeat ("Traduttore PDF" in every file's section); address clones ONLY by the ids expandPrototype returns, never by label.
- "Cartella Sorgente" (Drive Folder Reader) — input
driveItemId= the SOURCE folder. Outputfiles:[{ driveItemId, name, path }], sorted by path then name;pathis"a/b/c", or""for a file at the top of the source; an entry withpathUnknown: truecould not be placed (a sub-folder the user cannot read) and is saved at the top of the destination. - "Traduttore PDF" (PDF Translator, in "File #x") — set on the PROTOTYPE in Phase 1:
targetLanguage, and when askedstyle(free text: register and wording only),bilingual(on by default),glossary(Drive CSV/XLSX; rows for other target languages are skipped).sourceLanguagestaysauto.driveItemIdis set per clone. OutputstranslatedPdf,bilingualPdf,report. - "Salva Traduzione" (Save File, in "File #x") —
file← "Traduttore PDF".translatedPdf;destinationFolder= the DESTINATION folder (on the prototype);subPathper clone. Leavenameempty: the file keeps the translator's name. - "Salva Bilingue" (Save File, in "File #x") — the same, with
file← "Traduttore PDF".bilingualPdf. Deleted from the prototype in Phase 3 when the user does not want the bilingual PDF. - "Esito File" (Make Array, in "File #x") — collects the clone's two saved names; feeds "Esito Cartella".
- "Esito Cartella" (Make Array) — the collector: one
item_<k>per file, added byexpandPrototype. Executing it runs the whole folder.
Execution Strategy
You MUST complete all 5 phases in order: Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 5. Do NOT skip any phase.
Phase 1 = steps 1–5 · Phase 2 = 6–8 · Phase 3 = 9–10 · Phase 4 = 11–12 · Phase 5 = 13–15. Numbering is continuous; letter-suffixed steps are mandatory sub-steps.
Gates: Phase 1 ends with at most ONE requestUserDecision (only for what the message left open); Phase 2 ends with ONE requestUserDecision that confirms the file list before anything is spent; Phases 3 and 4 run without asking; Phase 5 reports.
Global rules:
- After
nodeExecutorthe system notifies on completion — do NOT pollgetNodeOutputin a loop. The folder run can take a long time (two translations at a time per worker); wait for the notification. - If
nodeExecutoranswers "Waiting for user approval", END YOUR TURN at once: the approval card resumes the flow. Never ask for approval in text. - On a tool error retry up to 3 times with corrected parameters; then ask the user. Never skip a step. Never send a failed call again unchanged: the third identical change is blocked and ends the turn.
- Every node operation names nodes by id (
nodeId, the prototype ids inconfig), never bylabel. applyCanvasDatais used once, at step 1. Later changes useexpandPrototypeor atomic tools (batchUpsertNodes,deleteNode).followUpis called only at step 15.
Phase 1: Setup
Gate keys: targetLanguage (required), style and glossary (optional). targetLanguage must be resolved by the end of step 5.
applyCanvasData→ IMMEDIATELY commit the template, before any other tool call.- Find the two folders. Attached Drive chips or
@mentionswin: the message says which is the source ("traduci la cartella X in Y" → X is the source, Y the destination); with two folders and no wording, the first is the source. OtherwisesearchDriveItemswith each name the user gave; 0 results → one shorter query; still nothing after 2 attempts → ask the user for that folder. Never retry the same query. The destination must be a different folder and must NOT be inside the source (the next scan would pick up the translations): if it is, STOP and ask for another destination. - Resolve the ids in ONE
nodeFinder({ queries: ["Cartella Sorgente", "Traduttore PDF", "Salva Traduzione", "Salva Bilingue", "Esito File", "Esito Cartella"], includeConfig: true })→readerId,protoTranslatorId,protoSaveTranslationId,protoSaveBilingualId,protoResultId,collectorId; andsectionFinder({ query: "File #x" })→protoSectionId. - Phase 1 gate. Do 4a, 4b, 4c, then 4d.
4a — extract what the ORIGINAL message already says. known.targetLanguage from an explicit language ("in inglese", "into German", "翻译成中文"): codes it, en, fr, de, es, ja, pt, zh, ko, ru, nl. known.style from an explicit style ("formale", "linguaggio semplice"). known.bilingual = false only when the user says they do not want the bilingual PDF. known.glossary from an attached CSV or XLSX Drive file named as the glossary. Print one line naming what you extracted and what you will still ask.
4b — glossary candidates, only when known.glossary is null. ONE searchDriveItems({ query: "glossar", type: "document" }) (it matches "glossario" and "glossary"); keep up to 5 results whose name ends in .csv or .xlsx as glossaryCandidates. No result → the glossary question offers only "Nessun glossario".
4c — ask ONLY what is still null, in ONE call. The target language is required; the style and the glossary are optional and always offer "none" as the recommended answer. Never call requestUserDecision more than once in Phase 1; if all three are known, do not call it. Keep a question only if its known.<key> is null.
Write the questions in the user's language.
4d — merge.
targetLanguage← known value / the answer; anotherTextanswer is accepted when it names one of the eleven languages. Still unresolved (cancelled, or no language) → STOP and say the translation needs a target language.style← known value / the answer:none→ no style;formal→ "formale, registro professionale";plain→ "linguaggio semplice, frasi brevi, parole di uso comune";otherText→ that text as the user wrote it (at most 500 characters). Cancelled or skipped → no style.glossary← known value / the answer:none→ no glossary; a candidate id → that file;otherText→ ONEsearchDriveItems({ query: "<one distinctive word of that name>", type: "document" }), exactly one.csvor.xlsxresult → that file, otherwise no glossary and say so in one line. Cancelled or skipped → no glossary.
- Write the setup in ONE
batchUpsertNodescall, every operation bynodeId:{ nodeId: readerId, input: { driveItemId: "<source folder id>" } };{ nodeId: protoTranslatorId, input: { targetLanguage: "<code>" } }plus, on the same operation,stylewhen a style was resolved (step 4d),bilingual: falsewhenknown.bilingualis false, andglossary: "<file id>"when a glossary was resolved;{ nodeId: protoSaveTranslationId, input: { destinationFolder: "<destination folder id>" } };{ nodeId: protoSaveBilingualId, input: { destinationFolder: "<destination folder id>" } }. The prototype carries these settings to every clone. Never writesourceLanguage(it staysauto) orllmModelId.
Phase 1 complete. Now proceed to Phase 2. Do NOT skip to Phase 5.
Phase 2: Scan and Confirm
nodeExecutor({ nodeId: readerId })— it only reads the folder. When it completes,getNodeOutput({ nodeId: readerId })→files.- Sort the entries by file extension (case-insensitive) into two lists, keeping the reader's order:
todo:pdf,doc,docx,odt,rtf,txt,md,ppt,pptx,odp.skipped: every other extension (spreadsheets, images, audio, video, archives), with the reason "not a document". A file whosepathhas more than 20 parts also goes toskipped, with the reason "more than 20 sub-folders deep": Save File creates at most 20 folder levels. Twotodofiles in the samepathwith the same name before the extension (d.pdfandd.docx) would save under the same name: keep thepdf, move the other toskippedwith the reason "same name as ". Whentodohas more than 50 files, keep the first 50 for this run (deferred= the rest).
- Phase 2 gate — confirm before anything is spent. ONE call:
Write it in the user's language; drop the "file saltati" sentence when skipped is empty and add " file restano per una seconda esecuzione" when deferred is not empty. cancel, a cancelled card or todo empty → STOP: say what was found and that nothing was translated.
Phase 2 complete. Now proceed to Phase 3. Do NOT skip to Phase 5.
Phase 3: One Section per File
- Bilingual off (
known.bilingualis false) →deleteNode({ nodeId: protoSaveBilingualId })first: no clone gets a bilingual save, and "Esito File" keeps only the translation. Bilingual on → skip this step. - Clone the prototype for EVERY file of
todoin ONE call, then lay the canvas out: The label is<name>alone for a file at the top (pathis"", orpathUnknown), andsubPathis then"". Bilingual off → leave theprotoSaveBilingualIdkey out of everyconfig. Do NOT passlayout: it would merge the per-file sections into one. Keep the result'sinstances(eachlabelwith itsnodeIdMap, prototype id → clone id): Phase 4 maps every failure back to its file through them. ThenautoLayout({})once.
Phase 3 complete. Now proceed to Phase 4. Do NOT skip to Phase 5.
Phase 4: Run the Whole Folder
nodeExecutor({ nodeId: collectorId })— ONE run: every clone's "Traduttore PDF" (as many at once as the translation runtime allows, the rest queued), then its saves and its "Esito File". A file that fails stops only its own section; "Esito Cartella" then ends without a result, which is expected. Wait for the completion notification.- Collect the outcome in ONE call:
nodeFinder({ filter: "failed", limit: 100 }). Map each failed node to its file through theinstancesof step 10 (the instance whosenodeIdMapholds that id). For each failed "Traduttore PDF" clone,getNodeOutput({ nodeId: "<clone id>" })→ the first line of its error (at most 20 reads; group the rest as "same error"). Every file with no failed node isdone: its two PDFs are in<destination>/<path>/.
Phase 4 complete. Now proceed to Phase 5.
Phase 5: Report
verification→ a checklist: one line per file with its outcome (done, orfailedwith the reason), then the files skipped (each with its reason) and the files deferred, and where the results are (<destination>/<path>/).- If any file failed: ONE
requestUserDecisionwith options "Riprova i file non riusciti" (retry) and "Va bene così" (done).retry→nodeExecutor({ nodeId: collectorId })again: the files already done are reused from their finished run, only the failed ones run again; then step 12 and step 13 once more. A file that fails twice stays failed. Whendeferredis not empty, say that a new workspace from this template on the same folders translates them (files already translated are replaced, not duplicated). followUp→ a short summary in the user's language: counts, the destination folder, and the files that need a person (failed or skipped).
Configuration Tips
- Style.
styleon the prototype's "Traduttore PDF" changes wording and register only ("formale, dai del Lei", "linguaggio semplice per pazienti"); a character voice comes out neutral, and the content never changes. - Glossary. One CSV can hold rows for several target languages (
source,target,tgt_lng); rows for other languages are skipped, and a glossary with no row for the target is not used (the report says so). - Bilingual. On by default: the reviewer sees each original page beside its translation, stamps and signatures included. Off saves only the translation (step 9).
- One file again. To redo a single file, execute that file's "Esito File" clone (its id from
instances): only that section runs.
Error Handling
batchUpsertNodesanswers "No node is labelled …" — an operation hadlabelinstead ofnodeId: send it again with the ids from step 3. Never send the same failed call a second time.expandPrototyperefuses aconfigkey — a key was not a prototype node id (a clone id, or a label): use theproto…Idvalues from step 3.- "Scanned PDFs (no text layer) are refused" — the file is an image scan: record it as failed ("scansione, da tradurre a mano").
- Untranslated text or changed numbers on page N — the translation failed its checks and nothing was saved for that file: record it as failed with the message; a retry often succeeds.
- The file has no PDF (422) or is still processing — Drive has no PDF of that item yet: record it as failed ("ancora in elaborazione"); the retry in step 14 picks it up once processing ends.
- Glossary errors (empty file, an incomplete row, one term with two translations) — every file fails the same way: say so once, tell the user to fix the glossary in Drive, and offer the retry after the fix.
- The destination folder cannot be written (permission denied) — every save fails: ask for another destination; then set it on the clones' saves with ONE
batchUpsertNodesand retry. - A sub-folder name Drive refuses (over 255 characters) — record the file as failed.
- Time limit — a very long file can hit the translator's time limit: record it as failed; split it with
pagesonly if the user asks.
// Dependencies
1from input import DriveFolderReader2from process import PDFTranslator3from output import SaveFile4from process import MakeArray$ git log --oneline