Effort Estimator
1. What? — Definition and context
Section titled “1. What? — Definition and context”The Effort Estimator is a family of four N8N workflows answering a recurring question: how many hours for this task? No LLM call, no guesswork — a deterministic calculation rooted in the tasks already completed in Odoo, for which both the initial estimate and the actual hours are known.
The principle is simple. Each Odoo task carries an x_estimated_hours field (manual or heuristic estimate) and a total of actual hours, aggregated from timesheets (account.analytic.line) or from Claude Code sessions. Once enough paired samples exist, the system extracts percentiles (P25 / P50 / P75) over a similar cohort and returns them as low / likely / high estimate.
The four workflows in the family
Section titled “The four workflows in the family”| Workflow | ID | Nodes | Role |
|---|---|---|---|
| Estimation Coverage Weekly | BABocZHi7nlZ1uU5 | 9 | Weekly digest tracking progress toward the 30 paired-sample target |
| Effort Estimator V1 | 2A03Nc8Co0Blr96m | 9 | Sub-workflow: cohort selection + percentiles + confidence score |
| Estimate Handler | ZzB58z0NVIntIDT3 | 7 | Handler for the /estimate <description> Telegram command |
| Odoo Project Picker | TMGWxV5BrVcGht4Z | 10 | Paginated Odoo project selector (reusable sub-workflow) |
Data sources
Section titled “Data sources”| Odoo source | Key fields | Usage |
|---|---|---|
project.task | x_estimated_hours, x_complexity, x_claude_category, x_github_repo, x_claude_sessions, x_claude_cost_total | Builds the training sample |
account.analytic.line | task_id, unit_amount | Actual hours (timesheets) |
project.task.claude.session | task_id, active_time_hours | Fallback: Claude Code session hours for tasks without timesheet |
The so-called “generic bucket” tasks (x_claude_category set, per-commit-type aggregates) are excluded: they are not individually estimable.
2. Why? — Stakes and motivations
Section titled “2. Why? — Stakes and motivations”The need
Section titled “The need”Without an estimator, every new issue lands without reference. Three immediate consequences:
| Without estimator | With Effort Estimator V1 |
|---|---|
| Rough estimates, optimism bias | Percentiles drawn from history, exposed live on Telegram |
| No visibility on estimated vs actual gap | Weekly coverage tracking toward the 30 paired-sample target |
| Single estimate, no interval | low / likely / high triplet (P25/P50/P75) + confidence |
Why deterministic and not LLM?
Section titled “Why deterministic and not LLM?”| Criterion | LLM | Deterministic percentiles |
|---|---|---|
| Reproducibility | Varies by model and temperature | Identical given the same data |
| Auditability | Black box | Cohort + samples visible |
| Cost | Token-based, recurring | Zero |
| Private data | Leaves the VPS | Stays internal |
| Quality below n < 30 | Acceptable by fluff | Honest: “low confidence” displayed |
The phase 2 gate: 30 paired samples
Section titled “The phase 2 gate: 30 paired samples”The #188 epic plans to unlock a semantic estimator (description matching) only when 30 (estimated, actual) pairs are available. Below that, noise dominates signal. The Estimation Coverage Weekly digest materialises this gate: every Monday 09:00 Europe/Paris, a Telegram message shows the current pair count, the week-over-week delta, and a “Phase 2 UNLOCKED” line when the threshold falls.
3. How? — Technical implementation
Section titled “3. How? — Technical implementation”Overview
Section titled “Overview”Effort Estimator V1’s three parallel queries
Section titled “Effort Estimator V1’s three parallel queries”The 2A03Nc8Co0Blr96m sub-workflow (9 nodes) fires three Odoo queries in parallel, then a Merge node waits for all three before passing control to the calculation. The pattern is shared with the weekly digest.
| # | Node | Type | Role |
|---|---|---|---|
| 1 | Execute Workflow Trigger | trigger | Passthrough input: description, project_id?, repo? |
| 2 | Odoo Get Paired Tasks | odoo | project.task getAll, filter x_estimated_hours != false, exclude generic bucket |
| 3 | Odoo Get Timesheets | odoo | account.analytic.line getAll, unit_amount > 0 |
| 4 | Odoo Get Sessions | odoo | project.task.claude.session getAll, Claude Code hours fallback |
| 5 | Wait All Queries | merge | Combine by position, 3 inputs |
| 6 | Build Estimate | code | Cohort selection, percentiles, score |
| 7 | Has Data? | if | Route empty sample |
| 8 | Respond (true) | code | Return estimated structure |
| 9 | Respond (false) | code | Return {error: "no_data"} |
The cohort cascade
Section titled “The cohort cascade”The Build Estimate Code node enforces a strict priority: it picks the first cohort containing at least 5 samples.
| Priority | Dimension | Source |
|---|---|---|
| 1 | project_id | Input parameter |
| 2 | x_github_repo | Input parameter |
| 3 | x_claude_category | Derived from description (keyword matching) |
| 4 | x_complexity | Derived from description (length + keywords) |
| 5 | Global | All paired samples |
Category derivation is intentionally short: doc/readme → docs, fix/bug/crash → fix, refactor/clean → refactor, etc. Same for complexity (short + typo/rename → simple, architect/migrat/redesign → complex, default medium).
The confidence score
Section titled “The confidence score”Three factors blend into a total:
| Factor | Weight | High (3) | Medium (2) | Low (1) |
|---|---|---|---|---|
| Sample size | 40% | n >= 20 | n >= 10 | n < 10 |
| Dispersion (IQR / median) | 30% | <= 0.5 | <= 1.0 | > 1.0 |
| Cohort proximity | 30% | project / repo | category / complexity | global |
Total >= 2.5 = high, >= 1.5 = medium, otherwise low. The confidence is always returned to the caller: useful for the Telegram Handler, which adds a warning when it lands low.
Output format
Section titled “Output format”{ "has_data": true, "estimated_hours_low": 0.5, "estimated_hours_likely": 1.0, "estimated_hours_high": 3.1, "expected_sessions": 2, "expected_api_cost": 0.31, "sample_size": 34, "confidence": "medium", "based_on": "repo=GuiGPaP/stacks_vps (n=34)"}When no usable sample exists, the payload falls back to {has_data: false, error: "no_data"} and the Handler returns a usage message rather than a fake number.
The /estimate Telegram journey
Section titled “The /estimate Telegram journey”The Estimate Handler (ZzB58z0NVIntIDT3, 7 nodes) accepts two input types, already extracted by the Telegram Orchestrator:
| Source | Input format |
|---|---|
/estimate <description> command | Raw text, parsed to strip the prefix |
| ”Estimer” menu button + ForceReply | Free reply from the Reply Action Handler |
The Extract Description node validates non-empty text, then calls Effort Estimator V1. The return is formatted as Telegram HTML:
Effort Estimate
Add pagination to /todo digest
Low Likely High0.5 1.0 3.1(hours)
Sessions: ~2 | API cost: ~$0.31Based on: 34 tasks (repo=GuiGPaP/stacks_vps (n=34))Confidence: mediumThe weekly digest
Section titled “The weekly digest”Estimation Coverage Weekly (BABocZHi7nlZ1uU5, 9 nodes) runs from a Schedule Trigger pinned to Monday 09:00 Europe/Paris. Its goal is to track progress toward the phase 2 gate.
| # | Node | Role |
|---|---|---|
| 1 | Schedule Trigger | Cron Monday 09:00 |
| 2 | Odoo Get Tasks | project.task getAll (limit 500), estimation + telemetry fields |
| 3 | Odoo Get Timesheets | account.analytic.line getAll (limit 2000) |
| 4 | DT Get Previous Report | Reads last row for delta calculation |
| 5 | Wait All Queries | Merge 3 inputs |
| 6 | Build Coverage Report | Join + metrics + HTML format |
| 7 | Has Data? | Skip if no active tasks |
| 8 | Call Notification Hub | type=digest, severity=info |
| 9 | DT Save Snapshot | INSERT into estimation_coverage_history |
The estimation_coverage_history Data Table (yBggBMumLMqfzQbv) stores one snapshot per week: report_date, total_tasks, estimated_count, paired_count, gate_ready. The delta shown in the Telegram message ([+3], [+2]) comes directly from this table.
What about the Odoo Project Picker?
Section titled “What about the Odoo Project Picker?”The fourth workflow, Odoo Project Picker (TMGWxV5BrVcGht4Z, 10 nodes), is a generic paginated selector. It is not a strict estimation component, but it can be wired into the Handler when the user wants an estimate scoped to a specific project: /estimate then opens a paginated Telegram view, the user taps on a project, and the chosen project_id is injected into the V1 call. It is shared with the /projet status routes and Odoo list pagination.
4. What if? — Outlook and limits
Section titled “4. What if? — Outlook and limits”Current limits
Section titled “Current limits”| Limit | Impact | Mitigation |
|---|---|---|
| Low sample volume | Confidence stays low below 30 pairs | Weekly digest watches the progress |
| Keyword-based matching | Coarse categorisation | Phase 2 plans semantic matching |
| No online learning | No incremental update per execution | Acceptable, percentiles are stable |
| Global cohort as fallback | One large task can drag the median up | Dispersion lowers the confidence |
Evolution scenarios
Section titled “Evolution scenarios”If coverage crosses 30 pairs:
- Phase 2 activation: semantic matching on title + body via embeddings
- Automatic comparison with the manual estimate at issue-opening time
- Display of “neighbour” tasks in the Telegram message for comparison
If volume explodes:
- Indexing by project to avoid the
limit=500full scan - Cohort caching in Redis with invalidation at each closure
- Pre-computed percentiles in an Odoo stats table
If multiple users:
- Additional per-contributor cohort (useful to personalise the estimate)
- Possibility to filter samples by personal velocity
- Interactive calibration (re-estimate a past task via Telegram)
Related pages
Section titled “Related pages”Workflows
Section titled “Workflows”- Claude Code Telemetry — Feeds
x_claude_sessions,x_claude_cost_totaland theproject.task.claude.sessionfallback - GitHub-Odoo Sync — Provides the GitHub-to-
project.taskpipeline (x_estimated_hours,x_complexityfields) - Daily Digests & Triage — Family of recurring digests that Estimation Coverage Weekly belongs to
Infrastructure
Section titled “Infrastructure”- Odoo 18 on Docker —
project_github_syncmodule andx_*fields
Reference
Section titled “Reference”- Glossary — Percentile, IQR, sub-workflow