$ cat workspace-template.yaml
Odoo to Ontology Feeder
// Syncs an Odoo Sales + Invoicing instance into the organization ontology: courses, learners, cohorts, enrollments and invoices, with the typed links between them. Idempotent — re-running upserts rather than duplicating. Use when a source system has no native Drive adapter and the workspace itself has to be the pipeline.
// Canvas Preview
// Instruction
Odoo to Ontology Feeder
Overview
Reads an Odoo Sales + Invoicing instance and loads it into the organization ontology as typed objects and links: Course, Learner, Cohort, Enrollment and Invoice, joined by cohort_of, enrollment_of, enrollment_in and billed_by. Every write is an idempotent upsert on a natural key, so running the graph twice updates rather than duplicates.
This template exists for the case where a source system has no native Drive adapter. Drive-resident content feeds the ontology natively; Odoo does not, so the workspace is the pipeline. It is a feeder, not an application — nothing here reads the graph back.
Odoo remains authoritative for money. What lands in the ontology is a reference plus last-known state (amounts, due date, payment state). Nothing in this graph posts an accounting entry.
Requires: an Odoo connection on the workspace-token owner's account (or ODOO_URL / ODOO_API_KEY workspace secrets), and the seven object types already created (scripts/demo-course-seller/seed_ontology_types.py).
Workflow Chain
Node Reference
- "Run Date" (String) — the ISO date the sync treats as today. Drives cohort
status(upcoming / running / completed) and the engagement window. The executor has no reliable clock, so this is supplied, never inferred. Configurable:value. - "Odoo Courses" (Odoo RPC) —
product.template.search_readfor sellable products with adefault_code. That code becomes the Course identity key. Configurable:params.domain,params.limit. - "Odoo Learners" (Odoo RPC) —
res.partner.search_readwherecustomer_rank > 0. Configurable:params.domain,params.limit. - "Odoo Orders" (Odoo RPC) —
sale.order.search_readfor confirmed orders (state in ['sale','done']). One order = one enrollment. Configurable:params.domain,params.limit. - "Odoo Invoices" (Odoo RPC) —
account.move.search_readfor posted customer invoices. Configurable:params.domain,params.limit. - "Upsert Courses" (Custom Code) — mints Course objects keyed by
default_code. OutputscourseIds(code → object id). Products without adefault_codeare skipped and named insummary. - "Upsert Learners" (Custom Code) — mints Learner objects keyed by lowercased email. Partners without an email are skipped and counted.
- "Upsert Cohorts & Enrollments" (Custom Code) — parses each order's
client_order_ref, which must read<email>::<COURSECODE>-<YYYYMM>. Derives the Cohort from that key, mints Cohort + Enrollment, and creates all three links. Configurable:runDate,syntheticEngagement. - "Upsert Invoices" (Custom Code) — mints Invoice objects keyed by the Odoo invoice number and links each to its Enrollment via
payment_reference(INV::<enrollment key>). - "Sync Report" (Make Array) / "Sync Result" (Preview) — collect and display the four stage summaries.
Execution Strategy
You MUST complete all 4 phases in order: Phase 1 → Phase 2 → Phase 3 → Phase 4. Do NOT skip any phase.
Loading the ontology is a write outside the graph. It is gated once, in Phase 1, and never re-confirmed.
Phase 1: Confirm scope
applyCanvasDatato commit the template, then usenodeFinderfor every id — ids from the template preview are stale.- Call
requestUserDecisionONCE, bundling both questions. Do not prompt again in any later phase.- key
runDate, question "Which date should the sync treat as today? It sets cohort status and the engagement window.",options: [](free text), suggest the value already baked into the "Run Date" node. - key
confirmWrite, question "This writes Course, Learner, Cohort, Enrollment and Invoice objects into the organization ontology. Proceed?",allowFreeText: false, optionsproceed/cancel.
- key
- If
confirmWriteiscancel, or the call returnscancelled: true, STOP and callfollowUpoffering to run it later. Do not execute any node. nodeFinder"Run Date" →upsertNode{ value: <runDate answer> }.nodeFinder"Upsert Cohorts & Enrollments" →upsertNode{ runDate: <runDate answer> }. Both must carry the same date; the String node feeds the port at runtime but the baked value is the fallback when the edge is missing.
Phase 1 complete. Now proceed to Phase 2.
Phase 2: Verify the sources
nodeExecutorthe four Odoo nodes together: "Odoo Courses", "Odoo Learners", "Odoo Orders", "Odoo Invoices". They have no interdependencies and run in one batch.getNodeOutputon each and readrecord_count. If ANY is0, STOP and report which one — an empty source silently produces an empty ontology, which looks like success. Do not continue to Phase 3.- If a node fails with "Odoo is not connected", the workspace-token owner has no Odoo connection. Report that exact cause and STOP; it is a configuration problem, not something to retry.
Phase 2 complete. Now proceed to Phase 3.
Phase 3: Load the ontology
Run in dependency order — each stage needs the id map the previous one produced. Do NOT run them as one batch.
nodeExecutor"Upsert Courses" and "Upsert Learners" (independent of each other — one batch is correct here).getNodeOutputboth; eachsummarymust report a non-zero count.nodeExecutor"Upsert Cohorts & Enrollments".getNodeOutputand readsummary. If it reports orders skipped for a malformedclient_order_ref, that is the enrollment key contract — report the count and the sample refs, and continue.nodeExecutor"Upsert Invoices".getNodeOutputand readsummary, including how many invoices linked to an enrollment.nodeExecutor"Sync Report", then "Sync Result".
Phase 3 complete. Now proceed to Phase 4.
Phase 4: Verify and report
- Call
verificationwith the four stage summaries. Report counts per object type, links created, and every skip the summaries named — a skipped record is the difference between a sync that worked and one that looked like it did. - Call
followUpoffering: (a) attach a schedule so the sync runs hourly, (b) re-run now, (c) open the ontology module to inspect what was loaded. Do NOT create the schedule yourself.
Configuration Tips
Error Handling
// Dependencies
// Variables
1runDate:2 type: undefined3 label: "Run date"4 description: "ISO date the sync treats as today. Drives cohort status."5 required: undefined$ git log --oneline