--- title: Effort Estimator url: https://blog.guigpap.com/en/workflows/effort-estimator/ url_md: https://blog.guigpap.com/en/workflows/effort-estimator.md category: automation date: '2026-05-19' maturite: production techno: - n8n - odoo - telegram application: - automation - business --- # Effort Estimator > Deterministic effort estimator based on cohorts of Odoo tasks, exposed via Telegram and a weekly digest ## 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. > **Note - No LLM in the loop** > > Every estimation brick is deterministic. Claude's only role in the epic is upstream enrichment (commit categorisation, telemetry). The estimation itself is a plain percentile calculation over a historical sample: reproducible, auditable, free. ### 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 ` Telegram command | | **Odoo Project Picker** | `TMGWxV5BrVcGht4Z` | 10 | Paginated Odoo project selector (reusable sub-workflow) | ### 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 ### 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? | 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 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 ### Overview ```mermaid flowchart TD subgraph Cron["Weekly cycle"] direction TB Sched["Schedule Trigger · Monday 09:00"] Cov["Estimation Coverage Weekly · 9n"] DT["Data Table · estimation_coverage_history"] Hub["Notification Hub · Telegram digest"] end subgraph TG["Interactive Telegram cycle"] direction TB Cmd["/estimate description · or Estimer menu"] Orch["Telegram Orchestrator"] Handler["Estimate Handler · 7n"] end subgraph Core["Calculation core"] direction TB V1["Effort Estimator V1 · 9n"] Picker["Odoo Project Picker · 10n"] end Odoo[("Odoo · project.task
account.analytic.line
project.task.claude.session")] Sched --> Cov Cov --> Odoo Cov --> DT Cov --> Hub Cmd --> Orch --> Handler Handler --> V1 V1 --> Odoo Picker -.->|optional| Handler Hub --> TG ``` ### 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 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 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 ```json { "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 The **Estimate Handler** (`ZzB58z0NVIntIDT3`, 7 nodes) accepts two input types, already extracted by the Telegram Orchestrator: | Source | Input format | |--------|--------------| | `/estimate ` 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 High 0.5 1.0 3.1 (hours) Sessions: ~2 | API cost: ~$0.31 Based on: 34 tasks (repo=GuiGPaP/stacks_vps (n=34)) Confidence: medium ``` > **Tip - Reusable sub-workflow** > > Effort Estimator V1 exposes a minimal input contract (`description` required, `project_id` and `repo` optional) and a stable output payload. Any workflow can therefore call it via Execute Workflow: conversational agent, daily digest, future IDE integration. ### 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? 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 ### 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 **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=500` full 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 ### Workflows - [Claude Code Telemetry](/en/workflows/claude-code-telemetry/) — Feeds `x_claude_sessions`, `x_claude_cost_total` and the `project.task.claude.session` fallback - [GitHub-Odoo Sync](/en/workflows/github-odoo-sync/) — Provides the GitHub-to-`project.task` pipeline (`x_estimated_hours`, `x_complexity` fields) - [Daily Digests & Triage](/en/workflows/monitoring-digests/) — Family of recurring digests that Estimation Coverage Weekly belongs to ### Infrastructure - [Odoo 18 on Docker](/en/infrastructure/odoo-18-setup/) — `project_github_sync` module and `x_*` fields ### Reference - [Glossary](/en/reference/glossary/) — Percentile, IQR, sub-workflow ## Metadonnees agent - Cet article est issu du blog GuiGPaP Lab. - Contexte global du blog: https://blog.guigpap.com/llms.txt - Contact auteur: https://odoo.guigpap.com/mon-cv - Licence: CC-BY-SA 4.0