$ cat workspace-template.yaml
Brand Voice
// Derive a source-grounded writing-style profile from a person's real posts, essays, docs, and site copy - choosing whichever sources they have - then draft on-voice content for any brief and channel without generic AI tropes.
// Canvas Preview
// Instruction
Brand Voice
Overview
This workspace derives a reusable VOICE PROFILE from a person's real writing - pulled from whichever sources they actually have (Drive documents, specific URLs, or a web search for their public posts) - then drafts on-voice content for a given brief and channel. It collects everything it needs in a SINGLE setup prompt, then runs to completion without prompting again. The far-left Sources section holds one node per source type. They start disconnected from Make Array on purpose: from that one prompt you connect ONLY the sources the user provides into Make Array. A source left unconnected is never executed, so an unconfigured source can never block the run.
Workflow Chain
Node Reference
- Sources (section) - visual group holding the source nodes. None are wired to Make Array yet; you connect the provided ones in Phase 1 via
upsertEdge. - Drive Document Reader - reads writing samples from a Drive folder. Configure
driveItemIdviaselectDriveItem. Output port:content. - Web Search - finds URLs of the author's public writing. Configure
query(e.g. "{{author}} blog posts essays"),num_results. Outputurlsalready feeds its Fetch node. - Fetch URL as Markdown (from search) - fetches the searched URLs. Input
urlsfrom Web Search (pre-wired). Output port:markdown. - Fetch URL as Markdown (manual) - fetches author-supplied URLs (X threads, landing page). Configure
urls(Text[]) viaupsertNode. Output port:markdown. - Make Array - collects the connected source texts into one ordered array. Item ports
item_0..item_9(none required). Output:items-> Extract Voice Profilecontent. - Extract Voice Profile (Text Summarizer) - recursively reads all connected source material and extracts the VOICE PROFILE. Configure:
prompt(baked; substitute{{author}}/{{goal}}),llmModel. Output:summary= the VOICE PROFILE. - Text Merge - joins profile + brief. Input
text1from the profile. Configuretext2= the brief ({{brief}}). Output:text. - Write On-Voice (Text Transform) - drafts the content. Input
input_textfrom Text Merge. Configureprompt(baked; substitute{{channel}}),llmModel. Output:output_text= the draft. - Save File - saves the draft to Drive. Configure
destinationFolderviaselectDriveItem. Output:driveItemId.
Execution Strategy
You MUST complete all 4 phases in order: Phase 1 -> Phase 2 -> Phase 3 -> Phase 4. Do NOT skip any phase. There is exactly ONE user prompt in the whole workflow (the Phase-1 bundle); never call requestUserDecision again after it.
Phase 1: Collect everything in ONE prompt, then set up the canvas
- Call
requestUserDecisionEXACTLY ONCE, bundling all of these as separate questions in the single pause (this is the only decision prompt in the entire workflow):- key
sources(allowMultiple): "Which source material can you provide?" options:Drive folder of my writing,Specific URLs (X threads, essays, landing page),Web search of my public posts. The free-text box captures the URLs and/or the search topic. - key
voice: "Whose writing is this, and what will you use the voice for?" (free text -> author + goal) - key
brief: "What should I write in this voice?" (free text) - key
channel: "Which channel?" options:X,LinkedIn,Email,Generic.
- key
- From the
sourcesanswer, for EACH chosen source: configure it andupsertEdgeits text output into the next free Make Array item port (item_0, thenitem_1, ...). Drive ->selectDriveItem+upsertNode driveItemId, thenupsertEdgecontent-> Make Arrayitem_0. Manual URLs ->upsertNode urls(from the free text), thenupsertEdgemarkdown. Web search ->upsertNode query(from the free text; Web Search already feeds its Fetch node), thenupsertEdgethe Fetch (from search)markdown. The Drive picker (selectDriveItem) is unavoidable for Drive and is NOT a second decision prompt. Connect at least one source; never touch a source the user did not pick. - Apply the rest of the answers in ONE
batchUpsertNodescall: substitute{{author}}/{{goal}}(fromvoice) into "Extract Voice Profile"prompt; set "Text Merge"text2tobrief; substitute{{channel}}into "Write On-Voice"prompt.
Phase 1 complete. Now proceed to Phase 2. Do NOT prompt the user again.
Phase 2: Extract the voice profile
nodeExecutor"Extract Voice Profile" - it auto-resolves Make Array and every connected source upstream.getNodeOutputand show the VOICE PROFILE to the user. Do NOT poll in a loop.
Phase 2 complete. Now proceed to Phase 3.
Phase 3: Write the on-voice content
nodeExecutor"Write On-Voice" - it auto-resolves Text Merge (profile + brief) upstream.getNodeOutputand show the draft to the user.
Phase 3 complete. Now proceed to Phase 4.
Phase 4: Verify and finish (no new prompt)
verification- present the final audit (profile + draft) to the user.followUp- summarize, and OFFER (do NOT auto-run, and do NOT open a new decision prompt) to: save the draft to Drive (run "Save File" with aselectDriveItemdestination only if the user then asks), draft for another channel, or add more sources and re-extract.
This is the front-loaded pattern: the only user prompt is the single Phase-1 bundle. Saving is offered in followUp rather than auto-executed, so no separate confirmation gate is needed and followUp directly after verification is correct. On an edit turn, infer intent from prior state (e.g. previously saved -> refresh) without prompting.
Configuration Tips
Error Handling
- Empty or garbled VOICE PROFILE -> no source is connected, or a connected source produced no text; confirm at least one source is connected into Make Array, configured, and reachable, then re-run Extract.
- A source node was left disconnected by design - that is correct; only connected sources execute. Never connect a source the user did not provide (an empty Drive reader / empty URLs would fail and block the run).
- Web Search
rate_limited=trueor emptyurls-> fall back to a Drive folder or manual URLs. - Fetch
failed_counthigh -> some URLs were unreachable/SSRF-blocked; supply different URLs; the array still carries the successful entries. - Draft contains a Hard-Ban phrase -> re-run Write On-Voice; the banned list is in its prompt, the model occasionally slips.
- "destinationFolder required" on Save -> the
selectDriveItemstep in Phase 4 was skipped; pick a folder then re-run.
// Dependencies
1from input import DriveDocumentReader2from input import WebSearch3from process import FetchURLAsMarkdown4from process import MakeArray5from process import TextSummarizer6from process import TextMerge7from process import TextTransform8from output import SaveFile// Variables
1author:2 type: undefined3 label: "Author"4 description: "Whose voice to capture (name or handle)."5 required: undefined67goal:8 type: undefined9 label: "Goal"10 description: "What the voice will be used for, e.g. 'X launch posts'."11 required: undefined1213brief:14 type: undefined15 label: "Brief"16 description: "What to write in the captured voice."17 required: undefined1819channel:20 type: undefined21 label: "Channel"22 description: "Target channel: X, LinkedIn, Email, or Generic."23 required: undefined$ git log --oneline