$ cat workspace-template.yaml

T

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.

Automation
#pdf#traduzione#translation#translate#cartella#folder#drive#batch#bilingue#bilingual#layout

// Canvas Preview

canvas.flow

// Instruction

instruction.md

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.

"Cartella Sorgente" (folder) → files [{driveItemId, name, path}]                 (read once, Phase 2)"File #x" (PROTOTYPE section, never run): "Traduttore PDF" ─translatedPdf→ "Salva Traduzione" ─name→ "Esito File".item_0                                                          ─bilingualPdf──→ "Salva Bilingue"   ─name→ "Esito File".item_1expandPrototype → one section per file, labelled "<path>/<name>", each clone's "Esito File" → "Esito Cartella".item_knodeExecutor("Esito Cartella") → every file translated and saved in ONE run

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

"Cartella Sorgente" (Drive Folder Reader, driveItemId = SOURCE folder)  → output files: one entry per file, { driveItemId, name, path } — path = the sub-folder under the source ("" at the top)
"File #x" — the PROTOTYPE section (configured in Phase 1, never run, never wired to "Esito Cartella"):  "Traduttore PDF" (PDF Translator)    ├─ translatedPdf → "Salva Traduzione".file   (destinationFolder = DESTINATION, subPath per clone)    └─ bilingualPdf  → "Salva Bilingue".file     (destinationFolder = DESTINATION, subPath per clone)  "Salva Traduzione".name → "Esito File".item_0 ;  "Salva Bilingue".name → "Esito File".item_1
expandPrototype: one clone of "File #x" per file (section label "<path>/<name>"),  config per clone: "Traduttore PDF".driveItemId, "Salva Traduzione".subPath, "Salva Bilingue".subPath  collector: each clone's "Esito File".items → "Esito Cartella".item_<k>
"Esito Cartella" (Make Array) — executing it runs every clone: translations in parallel, then the saves.

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. Output files: [{ driveItemId, name, path }], sorted by path then name; path is "a/b/c", or "" for a file at the top of the source; an entry with pathUnknown: true could 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 asked style (free text: register and wording only), bilingual (on by default), glossary (Drive CSV/XLSX; rows for other target languages are skipped). sourceLanguage stays auto. driveItemId is set per clone. Outputs translatedPdf, bilingualPdf, report.
  • "Salva Traduzione" (Save File, in "File #x") — file ← "Traduttore PDF".translatedPdf; destinationFolder = the DESTINATION folder (on the prototype); subPath per clone. Leave name empty: 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 by expandPrototype. 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 nodeExecutor the system notifies on completion — do NOT poll getNodeOutput in a loop. The folder run can take a long time (two translations at a time per worker); wait for the notification.
  • If nodeExecutor answers "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 in config), never by label.
  • applyCanvasData is used once, at step 1. Later changes use expandPrototype or atomic tools (batchUpsertNodes, deleteNode).
  • followUp is 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.

  1. applyCanvasData → IMMEDIATELY commit the template, before any other tool call.
  2. Find the two folders. Attached Drive chips or @mentions win: 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. Otherwise searchDriveItems with 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.
  3. 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; and sectionFinder({ query: "File #x" }) → protoSectionId.
  4. 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.

requestUserDecision({  questions: [    { key: "targetLanguage", question: "In quale lingua devo tradurre i file?", header: "Lingua",      context: "Ogni file viene tradotto mantenendo l'impaginazione; la lingua di partenza si riconosce da sola.",      options: [ { id: "it", label: "Italiano" }, { id: "en", label: "English", recommended: true }, { id: "de", label: "Deutsch" }, { id: "fr", label: "Français" }, { id: "zh", label: "中文" } ] },    { key: "style", question: "Che stile deve avere la traduzione? (facoltativo)", header: "Stile",      context: "Lo stile cambia il registro e le parole, mai il contenuto. Puoi anche scriverlo tu, per esempio «formale, dai del Lei».",      options: [ { id: "none", label: "Standard", description: "Traduzione professionale, senza indicazioni", recommended: true }, { id: "formal", label: "Formale" }, { id: "plain", label: "Linguaggio semplice" } ] },    { key: "glossary", question: "Vuoi usare un glossario? (facoltativo)", header: "Glossario",      context: "Un file CSV o XLSX in Drive con le colonne source, target e, se serve, tgt_lng: quei termini vengono tradotti sempre così.",      options: [ { id: "none", label: "Nessun glossario", recommended: true } /* then one { id: "<file id>", label: "<file name>" } per glossaryCandidates entry */ ] }  ]})

Write the questions in the user's language.

4d — merge.

  • targetLanguage ← known value / the answer; an otherText answer 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 → ONE searchDriveItems({ query: "<one distinctive word of that name>", type: "document" }), exactly one .csv or .xlsx result → that file, otherwise no glossary and say so in one line. Cancelled or skipped → no glossary.
  1. Write the setup in ONE batchUpsertNodes call, every operation by nodeId: { nodeId: readerId, input: { driveItemId: "<source folder id>" } }; { nodeId: protoTranslatorId, input: { targetLanguage: "<code>" } } plus, on the same operation, style when a style was resolved (step 4d), bilingual: false when known.bilingual is false, and glossary: "<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 write sourceLanguage (it stays auto) or llmModelId.

Phase 1 complete. Now proceed to Phase 2. Do NOT skip to Phase 5.

Phase 2: Scan and Confirm

  1. nodeExecutor({ nodeId: readerId }) — it only reads the folder. When it completes, getNodeOutput({ nodeId: readerId }) → files.
  2. 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 whose path has more than 20 parts also goes to skipped, with the reason "more than 20 sub-folders deep": Save File creates at most 20 folder levels. Two todo files in the same path with the same name before the extension (d.pdf and d.docx) would save under the same name: keep the pdf, move the other to skipped with the reason "same name as ". When todo has more than 50 files, keep the first 50 for this run (deferred = the rest).
  3. Phase 2 gate — confirm before anything is spent. ONE call:
requestUserDecision({  questions: [    { key: "start", question: "Traduco <N> file da <source> in <destination>, in <language>?", header: "Avvio",      context: "Ogni file avrà la sua sezione sul canvas e partono tutti con un solo avvio, due traduzioni alla volta. <K> file saltati: <up to 10 names, then 'e altri'>. Ogni file è una traduzione a pagamento.",      options: [ { id: "start", label: "Avvia la traduzione", recommended: true }, { id: "cancel", label: "Annulla" } ] }  ]})

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

  1. Bilingual off (known.bilingual is false) → deleteNode({ nodeId: protoSaveBilingualId }) first: no clone gets a bilingual save, and "Esito File" keeps only the translation. Bilingual on → skip this step.
  2. Clone the prototype for EVERY file of todo in ONE call, then lay the canvas out:
    expandPrototype({  prototype: { sectionId: protoSectionId },  items: [    { label: "<path>/<name>", config: {        "<protoTranslatorId>": { driveItemId: "<file driveItemId>" },        "<protoSaveTranslationId>": { subPath: "<path>" },        "<protoSaveBilingualId>": { subPath: "<path>" } } },    // …one entry per todo file, in order  ],  collector: { nodeId: collectorId, fromPrototypeNodeId: protoResultId, fromHandle: "items" }})
    The label is <name> alone for a file at the top (path is "", or pathUnknown), and subPath is then "". Bilingual off → leave the protoSaveBilingualId key out of every config. Do NOT pass layout: it would merge the per-file sections into one. Keep the result's instances (each label with its nodeIdMap, prototype id → clone id): Phase 4 maps every failure back to its file through them. Then autoLayout({}) once.

Phase 3 complete. Now proceed to Phase 4. Do NOT skip to Phase 5.

Phase 4: Run the Whole Folder

  1. 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.
  2. Collect the outcome in ONE call: nodeFinder({ filter: "failed", limit: 100 }). Map each failed node to its file through the instances of step 10 (the instance whose nodeIdMap holds 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 is done: its two PDFs are in <destination>/<path>/.

Phase 4 complete. Now proceed to Phase 5.

Phase 5: Report

  1. verification → a checklist: one line per file with its outcome (done, or failed with the reason), then the files skipped (each with its reason) and the files deferred, and where the results are (<destination>/<path>/).
  2. If any file failed: ONE requestUserDecision with 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. When deferred is not empty, say that a new workspace from this template on the same folders translates them (files already translated are replaced, not duplicated).
  3. 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. style on 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

  • batchUpsertNodes answers "No node is labelled …" — an operation had label instead of nodeId: send it again with the ids from step 3. Never send the same failed call a second time.
  • expandPrototype refuses a config key — a key was not a prototype node id (a clone id, or a label): use the proto…Id values 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 batchUpsertNodes and 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 pages only if the user asks.

// Dependencies

requirements.py
1from input import DriveFolderReader2from process import PDFTranslator3from output import SaveFile4from process import MakeArray

$ git log --oneline

v1.1.1
HEAD
2026-11-03
v1.0.12026-11-03
v1.0.02026-11-03