$ cat workspace-template.yaml
Learner Lifecycle Console
// An operations console for a course business, built on the organization ontology. Reads learners, cohorts, enrollments and courses from the graph, joins them to live Odoo invoices, and sorts every learner into one bucket: overdue, paid-but-inactive, at risk, ready to upsell, or healthy. Drafts re-engagement outreach, and on request writes the follow-up back to Odoo and stamps it on the ontology.
// Canvas Preview
// Instruction
Learner Lifecycle Console
Overview
The daily operating console for a course business. It reads the organization ontology — learners, cohorts, enrollments, courses — joins it to live Odoo invoices, and sorts every learner into exactly one bucket: overdue_unpaid, paid_inactive, at_risk, completed_upsell, healthy. It then drafts re-engagement outreach, and on request writes the follow-up back to Odoo and stamps it onto the ontology.
This is an application, not a pipeline. The ontology is the system of record it reads; Odoo is the system of engagement it reads money from and writes follow-ups to. The graph produces a decision and an action, not a dataset.
Division of authority, and it matters: the ontology answers who, what course, how far along. Odoo answers how much is still owed. AR is re-read from Odoo on every run rather than trusted from the ontology copy, so the console can never quote a stale balance.
Requires: the ontology already loaded (run Odoo to Ontology Feeder first) and an Odoo connection on the workspace-token owner's account.
Workflow Chain
Node Reference
- "Run Date" (String) — the ISO date every ageing rule reads. The executor has no reliable clock; nothing infers today. Configurable:
value. - "Read Ontology" (Custom Code) — lists Enrollment, Learner, Cohort and Course and joins them into flat rows, each carrying its
enrollmentIdso the action nodes need no second lookup. Configurable:maxObjects. - "Odoo Invoices (live)" (Odoo RPC) — posted customer invoices with residual and due date. Configurable:
params.domain,params.limit. - "Segment Learners" (Custom Code) — the business logic. Buckets are evaluated in priority order, first match wins, so counts sum to the population and nobody is chased twice. Outputs
kpis,segmentCounts,atRiskRows,overdueRows,upsellRows,revenueByCohort,atRiskText,summary. Configurable:staleDays(default 21). - "Console" (Preview) — shows the segmentation summary on the canvas.
- "Outreach Draft" (Text Transform) — one short re-engagement message per at-risk learner. Read-only: it drafts, it never sends. Configurable:
prompt,llmModelId(leave EMPTY for the workspace default). - "Mark Outreach Sent" (Custom Code) — GATED. Patches each enrollment with
status,outreachSentAt,outreachSegment. This is the workspace mutating the ontology. Configurable:maxRows,newStatus. - "Log Odoo Follow-up" (Custom Code) — GATED. One scheduled activity per learner on their Odoo contact. Configurable:
maxRows,note. - "Save Snapshot" (save-file) — GATED. Writes the drafted outreach to Drive.
destinationFolderships EMPTY on purpose; a baked folder id 422s on any other instance.
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.
There is exactly ONE requestUserDecision in this instruction, in Phase 4. It is deliberately NOT in Phase 1: every choice it offers depends on the segment counts, and asking which learners to contact before the numbers exist forces the user to answer blind. Phases 1–3 are entirely read-only, so nothing is committed before that gate.
Phase 1: Refresh
applyCanvasDatato commit the template, then usenodeFinderfor every id — preview ids are stale.- If the user named a date,
nodeFinder"Run Date" →upsertNode{ value: <that date> }, and set the same value on "Segment Learners"runDate. Otherwise keep the baked value; do NOT ask. nodeExecutor"Read Ontology" and "Odoo Invoices (live)" together — they are independent.getNodeOutputon both. If "Read Ontology" fails with "no Enrollment objects are visible", STOP: the ontology was never loaded. Report that the Odoo to Ontology Feeder must run first, and do not continue. If "Odoo Invoices (live)" returnsrecord_count: 0, continue but record it — every learner's AR state will read as unknown, and the console must say so rather than imply everyone has paid.
Phase 1 complete. Now proceed to Phase 2.
Phase 2: Segment
nodeExecutor"Segment Learners", then "Console".getNodeOutput"Segment Learners". Readkpis,segmentCountsandsummary.- Report the console to the user in plain prose, in this order: active learners and average progress; euros overdue and across how many invoices; the at-risk count split into
paid_inactivevsat_risk; the upsell count. Name the top three at-risk learners with their course, progress and days idle — the list is already sorted highest-value first, so the top of it is the work. - If
summaryreports enrollments with no matching Odoo invoice, say so explicitly and say what it means: their AR state is unknown, not zero. - If any row's
engagementSourceissynthetic, say that progress figures are derived rather than measured, because no Moodle completion feed is wired. Never present synthetic engagement as observed behaviour.
Phase 2 complete. Now proceed to Phase 3.
Phase 3: Draft outreach
nodeExecutor"Outreach Draft".getNodeOutputand show the user the first two drafted messages in full so they can judge the tone before anything is sent anywhere.- If
atRiskRowswas empty, "Outreach Draft" will report that nobody is at risk. Skip to Phase 4 and offer only the snapshot and the schedule.
Phase 3 complete. Now proceed to Phase 4.
Phase 4: The action gate
- Call
requestUserDecisionONCE, withkey: "actions",allowMultiple: true,allowFreeText: false, and these options. State the counts inside the question text so the user is choosing against real numbers:mark_ontology— "Stamp the outreach on N enrollments in the ontology (status → outreach_sent)"odoo_activity— "Create N follow-up activities on the learners' Odoo contacts"save_snapshot— "Save the drafted outreach to Drive"nothing— "Do nothing for now"
- If the result is
cancelled: true, ornothingis among the selections, take NO action and go to Phase 5. Never treat a cancelled gate as consent. - For each selected action, and ONLY those:
mark_ontology→nodeExecutor"Mark Outreach Sent".odoo_activity→nodeExecutor"Log Odoo Follow-up".save_snapshot→ callselectDriveItem(type=folder) so the user picks the destination, thennodeFinder"Save Snapshot" →upsertNode{ destinationFolder: <picked id>, name: "learner-outreach-<runDate>.md" }, thennodeExecutorit. Never invent a folder id and never reuse the empty default.
getNodeOutputeach action node that ran and read itssummary. Each one reports amaxRowscap when it truncated — repeat that number to the user verbatim. A capped run that reads as complete is the failure this guards against.
Phase 4 complete. Now proceed to Phase 5.
Phase 5: Verify and report
- Call
verificationcovering: what was read, how the population split, and exactly which side effects executed with their counts. If no action was selected, say plainly that nothing was written. - Call
followUpoffering: (a) attach a daily schedule so the console refreshes each morning, (b) open the Vista dashboard, (c) re-run for a different date, (d) drill into one learner's 360 in the ontology module. Do NOT create the schedule yourself.
Configuration Tips
Error Handling
// Dependencies
// Variables
1runDate:2 type: undefined3 label: "Run date"4 description: "ISO date treated as today by every ageing rule."5 required: undefined67staleDays:8 type: undefined9 label: "Stale after (days)"10 description: "Idle days before a running learner counts as at risk."11 required: undefined$ git log --oneline