$ cat workspace-template.yaml

O

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.

Automation
#odoo#ontology#sync#erp#pipeline

// Canvas Preview

canvas.flow

// Instruction

instruction.md

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

"Odoo Courses"[data] ──────────► "Upsert Courses"[courses]                                      │[courseIds]"Odoo Learners"[data] ─────────► "Upsert Learners"[partners]                                      │[learnerIds]"Run Date"[value] ─────┐              │"Odoo Orders"[data] ───┴──────► "Upsert Cohorts & Enrollments"[orders, courseIds, learnerIds, runDate]                                      │[enrollmentIds]"Odoo Invoices"[data] ────────► "Upsert Invoices"[invoices, enrollmentIds]
  all four [summary] ──────────► "Sync Report"[item_0..item_3] ──► "Sync Result"[value]

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_read for sellable products with a default_code. That code becomes the Course identity key. Configurable: params.domain, params.limit.
  • "Odoo Learners" (Odoo RPC) — res.partner.search_read where customer_rank > 0. Configurable: params.domain, params.limit.
  • "Odoo Orders" (Odoo RPC) — sale.order.search_read for confirmed orders (state in ['sale','done']). One order = one enrollment. Configurable: params.domain, params.limit.
  • "Odoo Invoices" (Odoo RPC) — account.move.search_read for posted customer invoices. Configurable: params.domain, params.limit.
  • "Upsert Courses" (Custom Code) — mints Course objects keyed by default_code. Outputs courseIds (code → object id). Products without a default_code are skipped and named in summary.
  • "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

  1. applyCanvasData to commit the template, then use nodeFinder for every id — ids from the template preview are stale.
  2. Call requestUserDecision ONCE, 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, options proceed / cancel.
  3. If confirmWrite is cancel, or the call returns cancelled: true, STOP and call followUp offering to run it later. Do not execute any node.
  4. 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

  1. nodeExecutor the four Odoo nodes together: "Odoo Courses", "Odoo Learners", "Odoo Orders", "Odoo Invoices". They have no interdependencies and run in one batch.
  2. getNodeOutput on each and read record_count. If ANY is 0, STOP and report which one — an empty source silently produces an empty ontology, which looks like success. Do not continue to Phase 3.
  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.

  1. nodeExecutor "Upsert Courses" and "Upsert Learners" (independent of each other — one batch is correct here). getNodeOutput both; each summary must report a non-zero count.
  2. nodeExecutor "Upsert Cohorts & Enrollments". getNodeOutput and read summary. If it reports orders skipped for a malformed client_order_ref, that is the enrollment key contract — report the count and the sample refs, and continue.
  3. nodeExecutor "Upsert Invoices". getNodeOutput and read summary, including how many invoices linked to an enrollment.
  4. nodeExecutor "Sync Report", then "Sync Result".

Phase 3 complete. Now proceed to Phase 4.

Phase 4: Verify and report

  1. Call verification with 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.
  2. Call followUp offering: (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

User saysConfigure
"sync only paid invoices""Odoo Invoices" params.domain → add ["payment_state","=","paid"]
"we have more than 1000 learners"raise params.limit on the relevant Odoo node — the template pins explicit limits, and the node's own default of 100 would truncate silently
"use real engagement data""Upsert Cohorts & Enrollments" syntheticEngagement"false". progressPct then stays 0 until a Moodle completion feed is wired
"sync a different date""Run Date" value AND "Upsert Cohorts & Enrollments" runDate — set both
"only sync one course""Odoo Orders" params.domain → add a client_order_ref like filter on the course code

Error Handling

ErrorCauseFix
Odoo is not connectedNo Odoo connection on the workspace-token owner, and no ODOO_URL/ODOO_API_KEY workspace secretAdd the connection in account settings → Connections. On a canvas the principal is the token OWNER, not the viewer
no ontology object types are visibleTypes never seeded, OR the token's user is not in ontology-review / emblema-adminRun seed_ontology_types.py; the type-listing endpoint returns an EMPTY list rather than 403 for a non-reviewer, so both causes look identical
object type 'X' not foundPartial seedRe-run seed_ontology_types.py; it is idempotent
runDate ... is not an ISO date"Run Date" holds a label instead of a dateSet value to YYYY-MM-DD
N orders skipped — client_order_ref is not ...Orders not created by this demo's seederExpected against real Odoo data. Either backfill client_order_ref, or change the enrollment key derivation in the node code
Invoices upserted but linkedCount is 0payment_reference does not carry INV::<enrollment key>Same contract issue as above, on the invoice side
ModuleNotFoundError: gaisExecutor image predates the gais SDKRebuild emblema/workspace-executor-python; reseeding the template alone will not fix it

// Dependencies

requirements.py
1from input import String2from process import OdooRPC3from process import CustomCode4from process import MakeArray5from control import Preview

// Variables

variables.yaml
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

v1.0.6
HEAD
2026-08-28
v1.0.32026-08-28
v1.0.22026-08-28
v1.0.12026-08-28
v1.0.02026-08-28