Skip to content

GitHub-Odoo Sync

The GitHub-Odoo Sync workflow turns every GitHub event (issue, PR, commit) into a matching Odoo operation: project task created, label applied, milestone synced, commit historised. Since the #274 refactoring, the architecture moved from a 138-node monolithic workflow to a 13-node hub that dispatches to 7 specialised sub-workflows.

WorkflowIDNodesRole
GitHub Project Sync (parent)JR8ESduxKNSQ7iVs13Trigger + mapping lookup + dispatch by event type
SW-1 Issue Lifecycle8yU07chLmZuogA3j23opened / closed / reopened + sub-issues parent
SW-2 Issue LabelsOMeuYvh0B9XA2S1H11labeled / unlabeled
SW-3 Issue Properties0BoSdwR4d8S9Fq0C32assigned / edited / pinned / renamed / deleted
SW-4 Issue MilestoneEZiZqgZNpxEzof7F13milestoned / demilestoned
SW-5 PR Handler3o7IzU2brzJeedB718pull_request opened / merged / closed
SW-6 Git EventsO2jirkOJOpOIy5ez18push (commits) + create (branch stage transition)
SW-7 Repo EntityWVWY01qmw9uYzSl828Label + Milestone CRUD at the repo level

A utility sub-workflow odoo-get-or-create-tag (j7e2EzEtIJ42T2S4) is called by SW-1, SW-2, and SW-7 to create or fetch Odoo tags matching GitHub labels.

Other sub-workflows

Issues sub-workflows

GitHub Project Sync · 13 nodes

GitHub · webhooks

Data Table · github_project_mapping

Github Trigger

Lookup Mapping

Sync Enabled?

Switch · Event Type

SW-1 Lifecycle · 23n

SW-2 Labels · 11n

SW-3 Properties · 32n

SW-4 Milestone · 13n

SW-5 PR Handler · 18n

SW-6 Git Events · 18n

SW-7 Repo Entity · 28n

odoo-get-or-create-tag

Odoo 18 · guig_db

Every GitHub event useful to a project workflow is synchronised:

CategoryEventsSub-workflow
Issue lifecycleopened, closed, reopenedSW-1
Sub-issuesparent_added, parent_removedSW-1
Issue labelslabeled, unlabeledSW-2
Issue propertiesassigned, edited, pinned, renamed, deletedSW-3
Issue milestonemilestoned, demilestonedSW-4
Pull requestsopened, closed, mergedSW-5
Push & branchespush (commits), create (branch)SW-6
Repo-levellabel/milestone CRUDSW-7

Without syncConsequence
Business visibilityIssues stay invisible from Odoo
Time trackingImpossible to tie a timesheet hour to a specific issue
Commit historyScattered across GitHub, no aggregated view per Odoo task
Double entryManual Odoo task creation for every issue

Why hub-spoke rather than a single workflow?

Section titled “Why hub-spoke rather than a single workflow?”

The original workflow (138 nodes) had grown along with each new event handled. Three symptoms triggered the #274 refactoring:

SymptomCauseConsequence
Slow editingN8N UI struggling with 138 nodesErgonomically expensive edits
Tedious testsTouching one sub-flow → re-test the wholeSilent regressions
CouplingAI tag, Odoo task, milestone code sharedLocal change broke distant branches

The hub architecture with 7 sub-workflows resolves those tensions: each SW has a narrow scope, can be edited and tested in isolation, and signs its I/O contract via Execute Workflow Trigger.

Why GitHub → Odoo and not the other way?

Section titled “Why GitHub → Odoo and not the other way?”
CriterionChoice
Source of truth for codeGitHub (where commits, PRs, CI live)
Source of truth for businessOdoo (timesheets, invoices, customer projects)
Trigger directionGitHub webhooks (real-time, sub-second)
EnrichmentOdoo adds business context (project, tags, complexity, estimate)

The reverse direction (closing an issue from Odoo) is technically feasible but introduces a loop risk. For now, GitHub stays the source; Odoo is sink + dashboard.


1. GitHub webhook — The N8N trigger receives the event with its X-Hub-Signature-256 HMAC signature (verified by N8N).

2. Mapping lookup — The hub queries the github_project_mapping Data Table to find the Odoo project tied to the repo (owner/repoodoo_project_id).

3. Switch event type — The hub routes to the appropriate sub-workflow based on headers.x-github-event and body.action.

4. Sub-workflow execution — Each SW receives {body, headers, query, mapping} in passthrough and applies its business logic.

5. Odoo XML-RPC calls — All SWs converge on the same Odoo endpoint: http://odoo:8069/xmlrpc/2/object, database guig_db.

The custom addon project_github_sync (v18.0.5.6.0) adds the fields needed by the sync. Every custom field uses the x_ prefix to avoid collision with Odoo natives:

FamilyFieldTypeSource
GitHubx_github_issue_idInteger (indexed)issue/PR number
GitHubx_github_urlChar(500)GitHub URL
GitHubx_github_repoChar (indexed)owner/repo
GitHubx_github_milestone_idIntegermilestone number
GitHubx_github_parent_issue_idIntegerparent (sub-issues)
GitHubx_github_commit_idsOne2manylinked commits
Effort (#188)x_estimated_hoursFloat(10,2)estimate at creation
Effort (#188)x_complexitySelectiontrivial / simple / moderate / complex
AI triage (#296)x_ai_project_triaged_atDatetimelast project analysis (TTL 7d)
AI triage (#296)x_ai_personal_triaged_atDatetimelast personal analysis (TTL 7d)
Telemetryx_claude_time_totalFloat(10,2)active Claude Code time
Telemetryx_claude_cost_totalFloat(10,4)API cost USD
Telemetryx_claude_token_totalIntegercumulative tokens
Telemetryx_claude_sessionsIntegersession count
Telemetryx_claude_lines_added/removedIntegerLOC delta
Telemetryx_claude_session_idsOne2manysession history
Telemetryx_claude_categorySelectionauto category for generic tasks
ModelRole
project.task.github.commitOne record per commit linked to a task (sha, message, author, date)
project.task.claude.sessionOne record per Claude Code session (id, duration, cost, tldr, category)

The busiest sub-workflow is SW-1 Issue Lifecycle, which handles 5 distinct actions:

parent_added · parent_removed

reopened

closed

opened

Execute Workflow Trigger · passthrough

Switch · body.action

Prepare Odoo Data + complexity + estimate

Convert MD to HTML

Detect Area Tag · area:xxx, feat(xxx)

Get/Create Tag

Create Task · stage Backlog

Prepare Closed Data

Find Task by x_github_issue_id

Is Project Task?

Close Project Task · stage 25 Done

Close Private Task · personal_stage 6 Done

Prepare Reopened Data

Find Task

Reopen → default_stage_id

Reopen Private → personal_stage 2 Today

Prepare Link/Unlink

Find Sub + Parent Tasks

Set parent_id + x_github_parent_issue_id

Clear parent_id

When a new issue is created, SW-1 derives two fields automatically from the title + body:

FieldCalculation
x_complexityHeuristic on body length, keywords (refactor, migration, multi-step)
x_estimated_hoursDeterministic model based on the complexity cohort (see Effort Estimator V1)

The initial estimate is a lower bound used as a reference. The effective_hours field (aggregated timesheets) then measures the gap between estimate and reality, feeding a weekly coverage report.

In parallel with event-driven sync, a daily scheduled workflow (61uBaaFQssxT1HJ0, 25 nodes) performs catch-up and AI triage:

StepAction
1. Catch-up syncFetches GitHub issues modified in the last 24h, updates Odoo
2. Stage promotionIf an issue is in progress on GitHub but Backlog on Odoo → promoted to In Progress
3. AI Issue Triage (#296)Sub-workflow AwqOyyAC3sUg9tSm (12n) — codex-yolo analyses the issue and proposes project stage + personal horizon
4. TTL 7d cacheThe x_ai_*_triaged_at fields prevent re-triaging an issue analysed recently

The AI triage is also exposed in interactive mode via the triage-interactive-api consumed by Claude Code during an open session (/triage command).

The hub passes a uniform object to every sub-workflow:

{
"body": { /* GitHub webhook payload */ },
"headers": {
"x-github-event": "issues",
"x-github-delivery": "..."
},
"query": {},
"mapping": {
"github_repo": "owner/repo",
"odoo_project_id": 5,
"default_stage_id": 21,
"sync_enabled": true
}
}

The input contract is strict (Execute Workflow Trigger in passthrough mode disabled for SW-3 to SW-7, which use typed workflowInputs).

To onboard an existing repo with hundreds of issues, the Initial Sync workflow (#50) allows a one-shot import: GitHub API pagination + sequential Odoo task creation + tag setup. Use it only at setup time; afterwards the event-driven sync is sufficient.


LimitImpactMitigation
UnidirectionalClosing on Odoo does not close the GitHub issueReverse sync to design with anti-loop flag
Manual mappingEach repo must be added to the Data TableAuto-discovery per organisation later
Initial estimate = lower boundComplex issues underestimatedWeekly recalibration planned via cohort matching
AI triage 24h max lagRe-triage the next day if issue ignoredAcceptable, triage is an assistant not an oracle

If bidirectional sync is needed:

  • automation.action Odoo webhook when a task moves to stage Done
  • The webhook posts a GitHub comment Closed via Odoo task #N
  • synced_from_odoo flag in the mapping table to block back-propagation

If many repos to manage:

  • Auto-discovery of repos from a GitHub organisation
  • Mapping templates by name convention (*-backend → Backend project)
  • Dedicated Odoo UI to configure mappings without touching the Data Table

If AI triage becomes critical:

  • Enable a high-confidence mode (auto-set the stage if confidence > 0.9)
  • Log decisions in claude_code_active_sessions for audit
  • Allow a manual override via /triage <issue> <stage>

  • Glossary — XML-RPC, Webhook, HMAC, Sub-workflow