Skip to content

Effort Estimator

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.

WorkflowIDNodesRole
Estimation Coverage WeeklyBABocZHi7nlZ1uU59Weekly digest tracking progress toward the 30 paired-sample target
Effort Estimator V12A03Nc8Co0Blr96m9Sub-workflow: cohort selection + percentiles + confidence score
Estimate HandlerZzB58z0NVIntIDT37Handler for the /estimate <description> Telegram command
Odoo Project PickerTMGWxV5BrVcGht4Z10Paginated Odoo project selector (reusable sub-workflow)
Odoo sourceKey fieldsUsage
project.taskx_estimated_hours, x_complexity, x_claude_category, x_github_repo, x_claude_sessions, x_claude_cost_totalBuilds the training sample
account.analytic.linetask_id, unit_amountActual hours (timesheets)
project.task.claude.sessiontask_id, active_time_hoursFallback: 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.


Without an estimator, every new issue lands without reference. Three immediate consequences:

Without estimatorWith Effort Estimator V1
Rough estimates, optimism biasPercentiles drawn from history, exposed live on Telegram
No visibility on estimated vs actual gapWeekly coverage tracking toward the 30 paired-sample target
Single estimate, no intervallow / likely / high triplet (P25/P50/P75) + confidence
CriterionLLMDeterministic percentiles
ReproducibilityVaries by model and temperatureIdentical given the same data
AuditabilityBlack boxCohort + samples visible
CostToken-based, recurringZero
Private dataLeaves the VPSStays internal
Quality below n < 30Acceptable by fluffHonest: “low confidence” displayed

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.


Calculation core

Interactive Telegram cycle

Weekly cycle

optional

Schedule Trigger · Monday 09:00

Estimation Coverage Weekly · 9n

Data Table · estimation_coverage_history

Notification Hub · Telegram digest

/estimate description · or Estimer menu

Telegram Orchestrator

Estimate Handler · 7n

Effort Estimator V1 · 9n

Odoo Project Picker · 10n

Odoo · project.task

account.analytic.line

project.task.claude.session

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.

#NodeTypeRole
1Execute Workflow TriggertriggerPassthrough input: description, project_id?, repo?
2Odoo Get Paired Tasksodooproject.task getAll, filter x_estimated_hours != false, exclude generic bucket
3Odoo Get Timesheetsodooaccount.analytic.line getAll, unit_amount > 0
4Odoo Get Sessionsodooproject.task.claude.session getAll, Claude Code hours fallback
5Wait All QueriesmergeCombine by position, 3 inputs
6Build EstimatecodeCohort selection, percentiles, score
7Has Data?ifRoute empty sample
8Respond (true)codeReturn estimated structure
9Respond (false)codeReturn {error: "no_data"}

The Build Estimate Code node enforces a strict priority: it picks the first cohort containing at least 5 samples.

PriorityDimensionSource
1project_idInput parameter
2x_github_repoInput parameter
3x_claude_categoryDerived from description (keyword matching)
4x_complexityDerived from description (length + keywords)
5GlobalAll paired samples

Category derivation is intentionally short: doc/readmedocs, fix/bug/crashfix, refactor/cleanrefactor, etc. Same for complexity (short + typo/rename → simple, architect/migrat/redesigncomplex, default medium).

Three factors blend into a total:

FactorWeightHigh (3)Medium (2)Low (1)
Sample size40%n >= 20n >= 10n < 10
Dispersion (IQR / median)30%<= 0.5<= 1.0> 1.0
Cohort proximity30%project / repocategory / complexityglobal

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.

{
"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 Handler (ZzB58z0NVIntIDT3, 7 nodes) accepts two input types, already extracted by the Telegram Orchestrator:

SourceInput format
/estimate <description> commandRaw text, parsed to strip the prefix
”Estimer” menu button + ForceReplyFree 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

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.

#NodeRole
1Schedule TriggerCron Monday 09:00
2Odoo Get Tasksproject.task getAll (limit 500), estimation + telemetry fields
3Odoo Get Timesheetsaccount.analytic.line getAll (limit 2000)
4DT Get Previous ReportReads last row for delta calculation
5Wait All QueriesMerge 3 inputs
6Build Coverage ReportJoin + metrics + HTML format
7Has Data?Skip if no active tasks
8Call Notification Hubtype=digest, severity=info
9DT Save SnapshotINSERT 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.

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.


LimitImpactMitigation
Low sample volumeConfidence stays low below 30 pairsWeekly digest watches the progress
Keyword-based matchingCoarse categorisationPhase 2 plans semantic matching
No online learningNo incremental update per executionAcceptable, percentiles are stable
Global cohort as fallbackOne large task can drag the median upDispersion lowers the confidence

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)

  • Claude Code Telemetry — Feeds x_claude_sessions, x_claude_cost_total and the project.task.claude.session fallback
  • GitHub-Odoo Sync — Provides the GitHub-to-project.task pipeline (x_estimated_hours, x_complexity fields)
  • Daily Digests & Triage — Family of recurring digests that Estimation Coverage Weekly belongs to
  • Glossary — Percentile, IQR, sub-workflow