$ cat workspace-template.yaml
Presentazione PPTX dalla Libreria Layout
// Crea una presentazione PowerPoint sul master del cliente dai documenti di una cartella Drive (o da un singolo file). Il nodo Libreria Layout legge la libreria di layout che il curatore ha pubblicato con un nodo Brand Master; l'agente sceglie il layout di ogni slide dalla libreria, scrive i contenuti, e il PPTX Builder compila il master vero, con anteprime PNG; il PDF Builder ne fa anche un PDF da condividere o stampare. Se il workspace ha già il suo Brand Master, usa quello (modalità locale). Niente viene pubblicato fuori dal workspace. Builds a PowerPoint deck on the customer's own master from the documents in a Drive folder (or one file). The Layout Library node reads the layout library a curator published with a Brand Master node; the agent picks every slide's layout from that library, writes the content, and the PPTX Builder fills the real master, with PNG previews; the PDF Builder also turns it into a PDF to share or print. A workspace with its own Brand Master uses that one (local mode). Nothing is published outside the workspace.
// Canvas Preview
// Instruction
Presentazione PPTX dalla Libreria Layout del Cliente
Template ID: assigned by each marketplace when the template is published (source: scripts/pptx-from-library/build_template.py).
Builds a PowerPoint deck on the customer's OWN master from the documents in a Drive folder (or one Drive file). "Libreria Layout" (Layout Library) reads the layout library a curator published with a Brand Master node in another workspace; in local mode (this workspace has its own "Brand Master") that Brand Master is the library instead. You plan every slide and pick its layout from that library by the layouts' descriptions and slot counts; "Contenuto Presentazione" writes all slide content in one pass; ONE expandPrototype call clones the slide prototype per slide and wires each clone to BOTH the content and the library (a feed ARRAY); Make Array → Presentation Composer (library + backgrounds) → PPTX Builder (presentation + library + master) produces the .pptx, and the PDF Builder turns that .pptx into a .pdf. Nothing here writes outside the workspace, so both run at the end without asking.
Units. Page = slide. A deck of P slides = 1 cover + P − 2 content slides + 1 closing, all clones of "Slide #x". When the user states a size ("una lezione di 12 slide"), P is the TOTAL.
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 lecturer (or any author) asks for a deck "on our template". The deck must use the customer's real PowerPoint master, so its slides are NOT the built-in template_name layouts: each Slide Generator runs in library mode — its library input is wired to the library node (libraryNodeId: "Libreria Layout", or "Brand Master" in local mode) and its layout input names one layout of that library (a layoutId, the layout's name in the master). There is no automatic layout in library mode: you choose every layout while planning, from the list the library node returns in its layouts output (step 7). The Slide Generator then writes slot JSON that must fit the layout's slots; text that does not fit fails the slide after 3 attempts, so pick layouts whose slots fit the content. The result is a .pptx file built by the PPTX Builder on the master itself, plus PNG previews, and the same deck as a .pdf from the PDF Builder.
Workflow Chain
Node Reference
Canvas labels are Italian; match them exactly with nodeFinder. After applyCanvasData the nodesPreview ids are dead: re-resolve every label. Port lists and defaults are in the schema reference in <current_workspace>.
- "Seleziona Materiale Sorgente" (Drive Folder Reader) — folder mode only;
driveItemIdtakes the FOLDER id; outputdriveItemIds. - "Leggi i Documenti disponibili" (Drive Content Reader) —
driveItemId→content, feeding "Selezione dei Topic Principali" and "Contenuto Presentazione". - "Libreria Layout" (Layout Library) — input
workspaceId(the workspace whose Brand Master publishes the library). Outputslibrary(the handle: enabled layouts, slots, capacities, library CSS),master(File),backgrounds(Image[]). Run it again to pick up the curator's latest publication. - "Brand Master" (local mode only; not in the template) — the curator's node:
driveItemId(the master) andcurationbelong to the curator, NEVER write them. Outputslibrary,master,backgrounds(same names and meaning as "Libreria Layout"; never wirecatalogorthumbnails). Running it publishes the master; it is a cache hit when nothing changed since its last run. - "Selezione dei Topic Principali" (Topic Selector,
content→summary) — configureprompt. LeavellmModelIdEMPTY. - "Contenuto Presentazione" (Content Generator,
content→summary) — configureprompt(rewritten at step 12). LeavellmModelIdEMPTY. - "Slide #x" (Slide Generator, prototype) — standalone, not in a section. Inputs
content,instruction,library(LayoutLibrary),layout(widgetlibrary-layout-picker: a layoutId of the wired library),language.template_nameis ignored in library mode. Outputslide_html. NEVER execute or delete the prototype, and never wire it to "Make Array". - "Make Array" — collector; pin order = slide order,
item_0= cover. Ships 10 pins;expandPrototypegrows the pins its items need. - "Presentation Composer" (
slides→presentation) — inputsslides,title,library,backgrounds. Withlibrarywired, its theme follows the library: do NOT settheme. It fails a slide that is not from this library (FOREIGN_SECTION). - "PPTX Builder" — inputs
presentation,library,master; outputspptx(File),previews(Image[]),previews_error(String). It checks every slide against the library and fails listing every problem by slide and slot. It writes nothing outside the workspace. - "PDF Builder" — inputs
presentation(Html) andpptx(File), ONE of them wired: herepptx← "PPTX Builder".pptx, so the PDF matches the PowerPoint file, andpresentationstays empty (both wired fails the node). Outputpdf(File, named after the.pptx), one page per slide. LibreOffice draws it, as it draws the previews: fonts can differ from PowerPoint, and hidden slides are left out. Executing it runs the PPTX Builder first. It writes nothing outside the workspace.
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–9 · Phase 3 = 10–12 · Phase 4 = 13–15 · Phase 5 = 16–20. Numbering is continuous; letter-suffixed steps are mandatory sub-steps.
Gates: Phase 1 ends with ONE requestUserDecision (only for what the message left open); Phases 2–4 run without pauses; Phase 5 audits, then executes "PDF Builder", which builds the .pptx and the .pdf (file-only outputs: no publish question).
Global rules:
- After
nodeExecutorthe system notifies on completion — do NOT pollgetNodeOutputin a loop. - On a tool error retry up to 3 times with corrected parameters; then ask the user. Never skip a step.
applyCanvasDatais used once, at step 1. Later changes useexpandPrototypeor atomic tools (upsertNode,upsertEdge,batchUpsertNodes,batchUpsertEdges,deleteNode,deleteEdge).followUpis called only at step 20.
Phase 1: Setup
Gate keys: slideCount, library. Both must be resolved by the end of step 5.
applyCanvasData→ IMMEDIATELY commit the template, before any other tool call.- Find the source: an attached Drive chip or
@mentionwins; otherwisesearchDriveItemswith the name the user gave. 0 results → one shorter query; still nothing after 2 attempts → ask the user for the item. Never retry the same query. - Configure the data source: if folder —
nodeFinder→ "Seleziona Materiale Sorgente",upsertNode({ nodeId: "<found ID>", input: { driveItemId: "<folder ID>" } }). If single file —nodeFinder→ "Seleziona Materiale Sorgente",deleteNodeit, thennodeFinder→ "Leggi i Documenti disponibili" andupsertNodethe samedriveItemIdthere. - Find the library candidates (no write yet).
nodeFinder({ queries: ["Brand Master", "Libreria Layout"], includeConfig: true }). Local mode — a "Brand Master" is on this canvas:libraryNodeId= its id,known.library = "local", keep the "Libreria Layout" id asunusedLibraryId, and skip the rest of this step. OtherwiselibraryNodeId= the "Libreria Layout" id. If itsworkspaceIdis already set (an admin pre-configured the template),known.library = <that id>and skip the rest of this step. Otherwise: the message names a library ("con la libreria CASD 2026") →listWorkspaces({ libraries: true, query: "<that name>" }); exactly one match →known.library = <its id>. No name, or 0 or several matches →listWorkspaces({ libraries: true }), keep the first 6 aslibraryCandidates; none → STOP: no library is available yet. Never pick a library yourself among several. Every call passeslibraries: true: a plainlistWorkspacesalso returns decks and tests, which are not libraries. The result is a WORKSPACE id: it goes only inworkspaceId(5d), never inuseWorkspaceTemplate. - Phase 1 gate. Do 5a, 5b, 5c, then 5d (and 5e in local mode).
5a — extract what the ORIGINAL message already says. Set known.slideCount from an explicit integer deck size ("12 slide", "a 12-slide deck", "12 页") — it is the TOTAL P, cover and closing included. Vague sizes ("una lezione breve", "circa 10") are NOT known. Print one line naming what you extracted and what you will still ask.
5b — ask ONLY what is still null, in ONE call. Never call requestUserDecision more than once in Phase 1; if nothing is missing, do not call it.
Write the questions in the user's language. Keep a question only if its known.<key> is null.
5c — merge. slideCount ← known value / answer (otherText accepted if it is an integer 3–40) [auto]. library ← known value / the chosen option id / an otherText name resolved with listWorkspaces({ libraries: true, query }) to exactly one workspace. Cancelled or unresolved library → STOP: say the deck needs the customer's library and ask which one to use. Do not continue without it.
5d — write the setup in ONE batchUpsertNodes call. Resolve ids with ONE nodeFinder call (queries: "Libreria Layout", "Selezione dei Topic Principali", "Contenuto Presentazione", "Slide #x"). Ops: { nodeId: libraryNodeId, input: { workspaceId: "<library>" } } (not in local mode: the Brand Master has no workspaceId), plus, when the deck language resolves, { input: { language: "<code>" } } on the three content nodes ("Selezione dei Topic Principali", "Contenuto Presentazione", "Slide #x" — the prototype passes it to every clone). The deck language is the language the user explicitly asked for, else the language of the request message; never ask, never infer it from the documents. Codes: it, en, zh, de, es, fr, ja, ko, nl, pt, ru. Unresolvable → leave language as is (auto).
5e — local mode only. deleteNode({ nodeId: unusedLibraryId }): "Libreria Layout" goes with its edges, so step 15 wires the Composer and the PPTX Builder from the Brand Master.
Phase 1 complete. Now proceed to Phase 2. Do NOT skip to Phase 5.
Phase 2: Library and Topics
nodeExecutor({ nodeId: libraryNodeId })— "Libreria Layout" reads the curator's latest publication; in local mode the Brand Master publishes this workspace's master. Wait for completion. If it fails, read the error withgetNodeOutput({ nodeId: libraryNodeId, keys: "master" })(errorMessagecarries[LIBRARY] {"code":…}), apply the matching row of## Error Handling, tell the user, and STOP: never plan layouts without the library.- Read the layouts from the library node's execution result:
getNodeOutput({ nodeId: libraryNodeId, keys: "layouts" })returns one line per enabled layout:layoutId "<id>" — label — description — slots — picture. Store it aslayouts; read it again whenever it is no longer in your context or the library node has run again. Nolayouts:statusFAILED → run the library node once more; otherwise → STOP: tell the user the layouts could not be read and the library node must be updated. Never callgetNodeOutputwithkeys: "library"(the handle carries the full catalog and CSS) and never guess a layoutId. Classify each layout once, from its label and description:cover(copertina, cover, titolo della presentazione),closing(chiusura, conclusione, riepilogo, grazie),index(indice, agenda, sommario),section(sezione, capitolo, divisore),table(a table slot),columns(a "fill exactly 2/3" group of side-by-side columns),text(one long text slot),bullets(a bullets slot),picture(picture: yes). - Configure and run the topic selector.
content_slides = P − 2whenslideCountis a number.nodeFinder→ "Selezione dei Topic Principali";upsertNode({ nodeId, input: { prompt: "Seleziona i <content_slides> argomenti principali dei documenti, uno per slide, nell'ordine in cui vanno presentati. Per ciascuno indica il titolo e il tipo di contenuto (spiegazione, elenco di punti, confronto tra due elementi, dati da tabella, sequenza di fasi)." } })— withauto, "Seleziona gli argomenti principali (al massimo 13)". ThennodeExecutorit ("Leggi i Documenti disponibili" runs as its dependency) and wait. getNodeOutput({ nodeId: "<topic selector id>", keys: "summary" })→ storetopics = [name, …]with each topic's content type, in order. More thancontent_slides→ keep the firstcontent_slides; fewer → keep them and say how many the documents support (never invent a topic).P = topics.length + 2. ALL slide titles used later come from this output.
Phase 2 complete. Now proceed to Phase 3. Do NOT skip to Phase 5.
Phase 3: Slide Plan and Content
-
Build the MAPPING TABLE — it is IMMUTABLE from here on. One row per slide,
pin0 …P − 1: pin 0 = cover, pins 1 …P − 2= the topics in order, pinP − 1= closing. For each row pick ONElayoutIdfromlayoutsby these rules, first match wins:- a. Cover (pin 0): the first
coverlayout. Never use a cover layout on any other pin. - b. Closing (last pin): the first
closinglayout; none → abulletslayout. - c. Index: only when the library has an
indexlayout AND its "fill exactly N" count equals the number of parts you will list (pick N parts from the topics). Otherwise no index slide. - d. Content slides, by the topic's content type: data →
table; two sides compared →columns(only with exactly the group's count of sides); phases or 3–6 parallel points →bullets; a definition or explanation →text; a new part of the lesson (≥10 slides) →section. - e. Never a layout with
picture: yes: this template wires no image into the slides, and an empty picture slot leaves a hole. - f. Fit: a long explanation needs a text slot with a large character limit; a slide with 6 points needs
≤6 itemsor more. A layout that says "fill exactly N" takes exactly N items: with another count choose atablewithin its row limit or atextlayout without a picture (rule g yields for that slide only), and never invent or drop an item to reach N. - g. Variety: use at least
min(6, number of usable layouts)distinct layouts whenP ≥ 10, and never the same layout on 3 consecutive pins. - h. Copy each
layoutIdEXACTLY as the JSON string shown (same case, spaces,+, accents); never translate or invent one.
Print the table:
pin | slide | title | layoutId | why (shape → layout). - a. Cover (pin 0): the first
- Write each row's
instructionnow (it goes into the clone at step 14):"Slide <n> di <P> — '<title>': <what the slide must say, from the topic>. Layout '<label>': <its slots and counts, e.g. 'titolo e 5 voci con numero, titolo e descrizione'>."For the cover: lesson title, subtitle, and the author/role only if the documents give them. nodeFinder→ "Contenuto Presentazione";upsertNode({ nodeId, input: { prompt: "Scrivi il contenuto di ogni slide della presentazione, nell'ordine:\n\n#Slide 1 (copertina): <title>\n#Slide 2: <title> — <layout label>: <slots and counts>\n…\n#Slide <P> (chiusura): <title>\n\nPer ogni slide scrivi solo i testi che il suo layout può contenere, brevi e completi, usando solo fatti dei documenti." } })— one line per mapping-table row with the exact titles. ThennodeExecutorit immediately and wait.
Phase 3 complete. Now proceed to Phase 4. Do NOT skip to Phase 5.
Phase 4: Slide Expansion
expandPrototype replaces clone → configure → wire → pins → layout with ONE atomic call: it clones "Slide #x" once per item, applies each item's config (keyed by the PROTOTYPE id), wires every feed in the feed array into every clone, wires each clone's slide_html to item_<pin> (adding missing Make Array pins), and lays the clones out. On error nothing changed: fix the arguments and retry. Do NOT re-run batchCloneNodes, organizeNodes, autoLayout or sectionManager over its result.
nodeFinder(ONE call,queries) →contentId("Contenuto Presentazione"),libraryNodeId("Libreria Layout", or "Brand Master" in local mode),protoId("Slide #x"),makeArrayId("Make Array"),composerId("Presentation Composer"),builderId("PPTX Builder"),pdfBuilderId("PDF Builder").- Expand — ONE call for all
Prows, every item with its explicitpinfrom the table: ThefeedMUST be the array of BOTH edges. A clone without thelibraryedge runs in built-in mode and the Composer rejects its slide. Keepinstances[i].nodeIdMap[protoId]= the clone id of rowi. - Wire the deck — check ALL seven edges, then create every missing one in ONE
batchUpsertEdgescall. Read the incoming edges of "Presentation Composer", "PPTX Builder" and "PDF Builder" from step 13'snodeFinderresult (never assume one is there, and never re-create one that exists: a second edge into the same input is an error):makeArrayId.items → composerId.slides,libraryNodeId.library → composerId.library,libraryNodeId.backgrounds → composerId.backgrounds,composerId.presentation → builderId.presentation,libraryNodeId.library → builderId.library,libraryNodeId.master → builderId.master,builderId.pptx → pdfBuilderId.pptx. ThenupsertNode({ nodeId: composerId, input: { title: "<lesson title>" } }).
Phase 4 complete. Now proceed to Phase 5.
Phase 5: Verification and Build
-
Audit the canvas, not your plan.
nodeFinder({ queries: ["Slide #", "Make Array", "Presentation Composer", "PPTX Builder", "PDF Builder"], includeConfig: true }). Run every check and fix every failure (one repair pass; then re-run the checks once):Print the roster
| pin | expected slide | actual source | status |. Still failing after one repair → STOP: do not execute; report each failure at step 19. nodeExecutor({ nodeId: pdfBuilderId })— it runs every slide, the Composer, the PPTX Builder and the PDF Builder. Wait for completion. If it fails: find the failed nodes (nodeFinder({ filter: "failed" })), read eacherrorMessage(getNodeOutput({ nodeId, keys: "slide_html" })) and apply## Error Handling. Only "PDF Builder" failed (the PPTX Builder completed) → the.pptxis ready: apply the PDF Builder row once, and never change a slide for it. A slide that does not fit gets ONE fix — a roomier layout of the same kind fromlayoutsviaupsertNode({ nodeId, input: { layout } }), or a shorterinstruction— then run step 17 once more. Never loop.getNodeOutput({ nodeId: builderId, keys: "pptx,previews_error" })→ confirm the.pptxexists. A non-emptypreviews_erroronly means the PNG previews are missing: the file is ready. ThengetNodeOutput({ nodeId: pdfBuilderId, keys: "pdf" })→ confirm the.pdfexists. 18a. Look at the slides — follow the skill Revisione Visiva (listed under<workspace_skills>: load it first,loadInstructionSection({ skill })with its pin), with: images node "Anteprima delle Slide" (the PDF, one image per page); record "Revisione delle Slide"; image n is slide n, its row'sshowsthe slide's title; all nine checks. The one fix allowed:upsertNodethat slide's cloneinstructionwith the concrete change (a shorter title, the number's unit, no filler item, the full address), thennodeExecutor({ nodeId: pdfBuilderId })again. A problem the layout causes is reported. The skill is not available (the list says so) → skip this step and say the PDF is on "Anteprima delle Slide" to check.verification— one item per check C1–C5 (state after the fixes), one for the visual check of step 18a (what is still open, per slide), one for the build ("PPTX pronto: " or the failure with its slide and slot), one for previews, one for the PDF ("PDF pronto: " or why it is missing). Summary in the user's language: slides delivered (the C4 roster count, not the plan), layouts used, library name.followUp— the deck is ready to download from "PPTX Builder" (.pptx) and "PDF Builder" (.pdf); suggest 2–3 next steps (change one slide's layout, add a slide, re-run "Libreria Layout" after the curator publishes again; in local mode, change layouts in the Brand Master's "Cura layout" and Pubblica).
Incremental Edits to an Existing Deck
Detection: the clones "Slide #n" and their Make Array edges existed BEFORE this turn → this is an EDIT. Do not call applyCanvasData, do not ask the Phase-1 questions, do not re-run Phases 2–4. A message that asks to build AND tweak is a build: run all phases, then apply the tweak before step 16.
- Change a slide's layout: pick the new layoutId from
layouts(same rules as step 10; step 7 if it is gone) →upsertNode({ nodeId: "<clone id>", input: { layout: "<layoutId>" } }). - Add a slide before the closing: move the closing up one pin (
deleteEdgeclosing →item_<k>,upsertEdgeclosing →item_<k+1>), then ONEexpandPrototypewith one item atpin: kand the same two-edgefeedarray, WITHOUTlayout(the section already exists). Leave the new clone next to the section:organizeNodes+autoLayoutare enough. NEVER callsectionManager(group)on nodes already in a section, and never retry it after "already in a section". Update "Contenuto Presentazione"'s prompt with the new slide and run it again. - Remove a slide:
deleteNodeits clone, then shift every later pin down by one (deleteEdge+upsertEdgeeach), keeping the closing last. - Reorder: swap the Make Array edges of the two clones.
- Only the PDF ("rifai il PDF", or it failed at step 17 while the
.pptxis ready):nodeExecutor"PDF Builder"; the slides and the PPTX Builder re-run only if they are stale. - New library publication:
nodeExecutor"Libreria Layout" again; its slides become stale and re-run on the next build. In local mode the curator's Pubblica already re-ran the Brand Master: the slides are stale the same way. Readlayoutsagain (step 7) and re-check C2.
Then run steps 16–20 (audit, build, verification, followUp).
Configuration Tips
Error Handling
Library errors arrive in errorMessage as [LIBRARY] {"code":"…","params":{…}}, possibly after other text.
// Dependencies
1from input import DriveFolderReader2from input import DriveContentReader3from process import TextSummarizer4from input import LayoutLibrary5from process import SlideGenerator6from process import MakeArray7from output import PresentationComposer8from output import PPTXBuilder9from output import PDFBuilder10from process import Snapshot// Skills
marketplace/revisione-visiva@1.2.0$ git log --oneline