Cleaning Policy Reference
A cleaning policy is written preset@version, for example default@2026-13. The preset decides which changes may be applied automatically; the version decides which rules and thresholds run. This page lists every step of each version in execution order, with its parameters, and explains every decision id and issue code in the cleaning report. The report's field layout and how to accept or decline decisions are in Cleaning Presets & Report.
1. Policies at a glance
| Policy | What it is | How to select it |
|---|---|---|
default@2026-13 | The current platform default. Every 2026-12 step, with the 10% missing-share block replaced by a check on where values are missing (see Missing-data rule) and an itemised health score. | Used when version is omitted (unless your key or organization sets one). |
default@2026-12 | Deprecated; will be removed after notice. Every 2026-11 step, plus a fixed forecast origin: up to 3 trailing empty values are treated as unknown instead of being dropped. | options.cleaning.version: "2026-12"; the response carries a cleaning_version_deprecated warning. |
default@2026-11 | Deprecated; will be removed after notice. Every 2026-10 step, plus per-gap imputation, physical constraints on filled values, the business-day grid B and extra data checks. | options.cleaning.version: "2026-11"; the response carries a cleaning_version_deprecated warning. |
default@2026-10 | Deprecated; will be removed after notice. The original frozen rule set, whose output is pinned byte for byte by regression fixtures. | options.cleaning.version: "2026-10"; the response carries a cleaning_version_deprecated warning. |
strict@2026-13 (deprecated: strict@2026-10, strict@2026-11, strict@2026-12) | Runs the same rules as default of that version, but applies only structural decisions automatically; the rest become proposals. | options.cleaning.preset: "strict", optionally with version. |
raw@2026-10 | No cleaning: a strict structural check only, then the data goes to the model as sent. | options.cleaning.preset: "raw". |
raw does not go through the cleaning service, and its structural check does not depend on the version; see section 7.
2. The versioning promise
- Dated versions are frozen. A released version's rules, thresholds and default parameters are never edited.
- Same input, same output. With the same data, options and policy
id, the cleaning result and the decisionids are the same. The one model-dependent step is the minimum-length check, which depends on the selected model's minimum context. - Rule changes ship as a new version. When the platform default moves to a new version, it is announced; the version it replaces is first marked deprecated and can still be selected explicitly (the response carries a
cleaning_version_deprecatedwarning) until it is removed after the announced date. - Defaults resolve by layer. The preset and the version are each taken from the first layer that sets them: request > key default > organization default > platform default. The platform default is currently
default@2026-13;2026-10,2026-11and2026-12are deprecated and will be removed after notice. Key and organization defaults are configured by the platform; there is no self-service setting yet. - The effective policy is always echoed. Successful responses carry
effective_policywithid,preset,versionandsource(request,key,orgorplatform: the highest layer that set the preset or the version). Recordeffective_policy.idto reproduce the same cleaning.
3. Selecting a version and the 2026-11 parameters
Pass version in options.cleaning only when you need a version other than the default 2026-13. The seven parameters below exist from 2026-11 on, so they work under the default too, and also go in options.cleaning:
{
"model": "hora-1.0",
"h": 24,
"freq": "1h",
"series": [
{"unique_id": "pv-01", "ds": "2026-06-01T00:00:00Z", "y": 0.0},
{"unique_id": "pv-01", "ds": "2026-06-01T01:00:00Z", "y": null}
],
"options": {
"cleaning": {
"preset": "default",
"impute_method": "auto",
"nonnegative": "true",
"bounds": {"lower": 0, "upper": 500},
"structural_zero": "auto",
"business_days": "off"
}
}
}(series is shortened; a real request needs at least the model's minimum context.)
| Parameter | Values | Default | Effect |
|---|---|---|---|
impute_method | auto, linear, pchip, seasonal | auto | auto picks a method per gap (see Per-gap imputation); a forced method that cannot run falls back to linear, and the report says why. |
seasonal_period | integer 2 to 10000 (grid steps) | inferred from the frequency | Sets the seasonal period. A given period only needs the series to span 2 cycles; autocorrelation is not tested. |
nonnegative | auto, true, false | auto | auto: with at least 8 observations and none negative, filled values are clipped at 0. true: always clip, and flag negative observations. false: off. |
bounds | {"lower": …, "upper": …} | none | Declared physical range: filled values are clipped into it, observed values outside it are flagged (out_of_bounds). At least one bound, and lower ≤ upper. |
structural_zero | auto, off | auto | auto: in seasonal phases that are almost always 0, filled values stay 0. |
business_days | auto, on, off | auto | auto: daily data with no weekend dates uses the business-day grid B. on: force B for daily data. off: keep calendar days. |
long_gap | fill, mask | fill | How the model reads interior gaps longer than the short-gap limit (the long_gap issue). fill: it reads the filled values. mask: the filled values and the report stay the same, but a model that supports missing-value masking reads those positions as missing. See Long gaps: fill or mask. |
Long gaps: fill or mask
Cleaning always fills every gap so the report and the cleaned series are complete. For long interior gaps, long_gap decides what the model reads:
fill(default): the model reads the filled values, exactly as shown in the report. Keep it when the filled values are a reasonable stand-in, for example when the series follows a stable seasonal pattern through the gap.mask: the model reads those positions as missing and forecasts from the observed values around them. Consider it when a long gap is an outage, a shutdown or another period whose real values are unknown and should not guide the forecast.maskis mainly meant for long gaps well before your most recent data; when a long gap ends close to the last observed value, keepfillunless a backtest on your data shows otherwise.
Short gaps (up to the short-gap limit) are filled for every model either way. With mask, each entity's model_input[] entry reports long_gap_points (masked long-gap positions in the context handed to the model) and long_gap_handling (how the model actually read them). SequenxAI Omni (hora-1.0), the model the API serves, supports missing-value masking: those positions reach it as missing values and long_gap_handling is "mask". "fill" applies only to a model that cannot mask missing values; it then reads the filled values instead (long_gap_handling: "fill", long_gap_reason: "model_without_mask", and a long_gap_mask_unavailable warning) and the request does not fail. The model the API serves today never takes this path. Billing is the same for both values: masked positions were never observed, so they are not billed input points. The choice is echoed in cleaning_report.policy.long_gap when you send it. Try both on your own data with a backtest before you settle on one.
Validation:
- Under
2026-10, any of these seven parameters (exceptnonnegative: "auto") returns 422CLEANING_OPTION_UNSUPPORTED; they are never silently ignored:
{
"error": {
"code": "CLEANING_OPTION_UNSUPPORTED",
"message": "Cleaning options require policy version 2026-11",
"details": {
"options": ["business_days"],
"policy": "default@2026-10",
"requires_version": "2026-11"
}
}
}nonnegative: "true"or"false"under2026-10also returnsCLEANING_OPTION_UNSUPPORTED, withoptionsandsupportedindetails.- Invalid values (for example
lowergreater thanupper) return 422VALIDATION_ERROR. - A field not listed for
options.cleaningreturns 422VALIDATION_ERROR(details.errors[].fieldnames it); it is never ignored. - An unknown
versionreturns 422CLEANING_OPTION_UNSUPPORTED;details.allowedlists the versions. - The
rawpreset accepts none of these parameters, and noaccept/decline. freq: "B"is valid only from2026-11on;2026-10returnsFREQ_UNSUPPORTED, andrawreturnsRAW_INPUT_INVALID.
From 2026-11 on, cleaning_report.policy echoes the resolved parameters: impute_method, structural_zero and business_days echo auto when omitted; seasonal_period, bounds and long_gap appear only when sent. Under 2026-10 the policy object keeps its previous shape. You can preview the parameters for free with /v1/clean.
4. Steps in default@2026-10
Each entity (unique_id) is processed independently, in this order. Position lists in the report hold at most 50 entries; beyond that truncated is true.
4.1 Parse timestamps and time zones
- Accepted timestamp formats are listed in Timestamp formats. When the day/month order cannot be proven from the data, the request fails with
AMBIGUOUS_DATE_FORMAT; passoptions.ds_format. - Timestamps with an offset are read with that offset; timestamps without one are read in
options.timezone, or in UTC when it is not set (issuenaive_timestamps_assumed_utc). - An unparseable timestamp fails the request with
VALIDATION_ERROR(2026-10reports the first one only). - Daylight-saving effects are reported, never changed:
dst_missing,dst_duplicate,dst_nonexistent_time,mixed_timezone. For hourly or finer data read entirely as UTC, a gap or repeat of exactly one hour on a Sunday between 00:00 and 03:00 UTC in March, April, September, October or November also raises adst_missing/dst_duplicatehint and adst_suspectedwarning.
4.2 Sort
Rows not in time order are stably sorted (decision time.reorder, issue out_of_order).
4.3 Recognize missing values
- Missing-value tokens. Text cells equal to
na,n/a,nan,null,none,?,#n/a,#value!or the empty string (case-insensitive) become null (decisionvalue.tokens). - A lone dash
-or--may mean 0 or missing: it is read as missing, and reading it as 0 is proposed (decisionvalue.dash_token,proposed). - Missing-value codes. The candidates are
9999,-9999,99999,-99999,-999,999999,65535and-32768. A candidate is nulled automatically when it lies outside the range of the other observations by more than the larger of their 1st-to-99th percentile spread and 10 MAD, and makes up at most 5% of the points; otherwise it is only proposed and the value is kept (decisionvalue.sentinel).
The two text rules apply only to text cells (for example an uploaded CSV); in JSON requests y is a number or null, so they normally do not fire.
4.4 Determine the frequency
- A
freqyou pass is used as is. - When omitted it is inferred: first as a calendar frequency anchored to months, quarters or years (median spacing of at least about 28 days, and at least 90% of stamps on the same day of month, such as the 1st, the month end or a fixed day); otherwise as the most common spacing, treating spacings within 2% or 1 second of each other as equal.
- A mode share of at least 90% is high confidence; 70% to 90% is low confidence, which raises a
freq_low_confidencewarning but does not block; below 70% the request fails withFREQ_INFERENCE_FAILED. - When the second most common spacing makes up at least 25% of all spacings and at least two thirds of the most common one, the frequency is ambiguous and the request fails with
FREQ_AMBIGUOUS. Entities inferring different frequencies whenfreqis omitted also returnFREQ_AMBIGUOUS.
4.5 Align to the grid
- Stamps slightly off a slot are snapped to the nearest slot (decision
grid.snap, issuetimestamp_jitter). Tolerance: 2% of the step for fixed steps (never more than half a step), for example 72 seconds for hourly data; ±12 hours for calendar frequencies. - Rows beyond the tolerance are dropped (decision
grid.off_grid, issueoff_grid, warningoff_grid_dropped). When more than 10% of an entity's rows are off-grid, the request fails withFREQ_INFERENCE_FAILED.
4.6 De-duplicate
When several rows land on the same slot after alignment, the last one is kept (decision time.duplicates, action keep_last). If the rows carry the same value the class is structural; if the values differ the class is drop and a duplicate_conflict warning is raised. At least 3 conflicting duplicates that also exceed 5% of the entity's rows add an entity_column_suspected issue: several series may be mixed under one unique_id.
4.7 Trim edge nulls
Nulls at the start and end of a series are dropped (decision gap.edge_trim, issue edge_null). An entity with no values at all fails with SERIES_EMPTY. From 2026-12 on, trailing nulls are handled differently; see Fixed forecast origin.
4.8 Model context window
History is never cut to fit the model. An entity longer than the selected model's maximum context is rejected with 422 SERIES_TOO_LONG before cleaning (distinct timestamps) or after it (the regular grid, filled slots included); send only the most recent points. The engine's window.context decision and context_window issue therefore never appear in API responses. The model's limits are in Models & Limits.
4.9 Split at a long gap
- When a gap between observations reaches the threshold, only the latest segment after the last long gap is kept (decision
gap.split). - Threshold:
options.gap_split_min_stepswhen passed (0disables the split); otherwise the largest of the short-gap limit plus one (4 steps), 3 seasonal cycles, and 5% of the series span. - Seasonal cycle length in steps: one day for sub-daily data, one week for daily up to weekly steps, one year for calendar frequencies, and 1 for weekly or longer fixed steps.
- The latest segment must cover at least the model's minimum context and, with the automatic threshold, at least 3 seasonal cycles; otherwise nothing is split.
- The split is applied when it drops at most half the observations; beyond that it is only proposed (issue
segment_split_skipped) and the gap is filled instead. - No split runs with
clean_series: false.
4.10 Missing-share check
Missing share = (stamps missing from the grid + null values) ÷ grid points, computed before any filling. Below 1% is Healthy, 1% to 10% is Warning, and 10% or more is Critical, which fails with SERIES_DISCONTINUOUS (blocking health_critical). 2026-13 replaces this check; see Missing-data rule.
The health score health.score (0 to 100) = 100 − min(40, 400 × gap share) − min(20, 200 × missing share) − min(15, 5 × conflicting duplicates) − min(15, 100 × outlier share), rounded. A Critical entity scores at most 49.
4.11 Insert empty slots
Stamps missing from the grid are inserted as empty slots (decision grid.regrid, issue gap).
4.12 Detect outliers (flag only, never modified)
- Rolling median / MAD: the window is 1/50 of the points, clamped to 21 to 101 and made odd; where the window holds at least 5 observations and a MAD above 0, points scoring 0.6745 × |x − median| ÷ MAD above 3.5 are flagged (issue
outlier, warningoutlier_flagged_not_modified). - Calendar-frequency data spanning at least 3 full yearly cycles first has the median of the same season in the other years subtracted, so a regular yearly peak is not an outlier.
- Outliers reach the model with their original values;
metrics.outlier_points_suppressedis always 0.
4.13 Detect flat runs and level shifts (flag only)
- Flat runs
flat_run. A run of identical non-zero observations reaching the minimum length is flagged. The minimum is half a seasonal cycle, clamped to 8 to 288 steps for fixed steps, with a floor of 4 steps for calendar frequencies. Series whose observations are all non-negative integers with a 90th percentile of at most 20 (small counts) are not checked. - Level shifts
level_shift. Adjacent windows whose medians differ by a factor of about 1000 (log ratio within 3 ± 0.25) are flagged; this usually means a unit mix-up. The window is 1/4 of the points, clamped to 5 to 48.
4.14 Fill gaps
- Every missing point (inserted slots and nulls) is filled by linear interpolation between its neighbours (decision
gap.impute, actionlinear). - A run of more than 3 missing steps (the short-gap limit) is still filled, with issue
long_gapand warninglong_gap_filled. - The
imputedaction'sgaps[]lists each gap's range,method,tierandreason. In2026-10the tier only describes the gap length and does not change the method (P is the seasonal cycle length from section 4.9, n the number of points):
| Tier | Gap length | reason |
|---|---|---|
T0 | ≤ 3 steps | within_gap_fill_max_steps |
T1 | ≤ P | no_seasonal_method |
T2 | ≤ max(3P, 5% of n) | no_seasonal_method |
T3 | longer | exceeds_long_gap_max |
- With
clean_series: falsenothing is filled; if any repair is needed the request fails withTIMESTAMP_IRREGULARorSERIES_DISCONTINUOUS(blockingcleaning_required).
4.15 Minimum length
With fewer points than the model's minimum context after cleaning, the request fails with SERIES_TOO_SHORT (blocking too_short_for_model).
5. What default@2026-11 adds
2026-11 runs every step of section 4 in the same order with the same thresholds, and adds rules at these points.
Timestamp parsing (step 4.1)
Every unparseable timestamp is listed in one VALIDATION_ERROR: details.cleaning_report.blocking[0] is unparseable_timestamp, with the request row numbers in rows (at most 50) and up to 5 examples.
Number-format checks (step 4.3)
Text cells that are not plain numbers are reported by kind and stay missing rather than being guessed (read_as: "missing"):
| Issue code | Example |
|---|---|
thousands_separator | 1,234.5 |
decimal_comma | 12,5 |
ambiguous_separator | 1,234 (thousands separator or decimal comma cannot be told apart) |
non_numeric_value | 12kW |
number_format_mixed | thousands separators and decimal commas in the same series |
Business-day grid B (step 4.4)
- With
business_days: "auto"andfreqomitted, daily data meeting all of these conditions uses the business-day grid: at most 1.05 stamps per date on average; no Saturday or Sunday dates; at least 15 weekday dates; a span of at least 21 days; weekday coverage (weekdays with data ÷ weekdays in the span) of at least 90%. Dates are read in the entity's local time (options.timezone, else the most common offset in the data). - On
B, weekends are not gaps and forecast stamps skip them (decisiongrid.business_days, classstructural; issuebusiness_days_pattern, whosecountis the number of weekend days skipped). There is no holiday calendar: a missing weekday holiday is still a gap. business_days: "on"forcesBfor daily data (whenfreqis omitted or daily), and weekend rows are dropped as off-grid; whenfreqis omitted and the data is not daily,business_days_not_applicableis reported."off"always keeps calendar days.- You can also pass
freq: "B"directly. Underauto, no business-day detection runs when you passfreq. - Decline
grid.business_days:<unique_id>to keep calendar days; weekends then become gaps.
Real output: 20 consecutive weekdays from Monday 2026-09-07, no freq:
"freq": {"inferred": "B", "used": "B", "source": "inferred",
"status": "HIGH_CONFIDENCE", "confidence": 1.0, "candidates": []}
{"id": "grid.business_days:fx", "rule": "grid.business_days", "class": "structural",
"status": "applied", "action": "business_day_grid",
"before": {"freq": "D"}, "after": {"freq": "B"}, "reversible": true,
"evidence": {"weekday_dates": 20, "weekend_dates": 0, "span_days": 26,
"coverage": 1.0, "skipped_weekend_days": 6, "source": "detected"}}
// forecast stamps for h=3: 2026-10-05, 2026-10-06, 2026-10-07 (Mon to Wed)Under 2026-10 the same data is inferred as calendar days (low confidence), the 6 weekend days become gaps, the missing share exceeds 10%, and the request fails with SERIES_DISCONTINUOUS.
Constant-series check (after step 4.12)
Observations that never change raise a constant_series warning; all zeros raise all_zero_series.
Seasonality test
Each entity's report carries series[].seasonality: {period, source, acf, cycles, accepted, reason}.
- The candidate
periodcomes from the frequency: steps per day for sub-daily data (24 for hourly), steps per week for daily up to weekly steps (7 for daily), 5 for business days, steps per year for weekly or longer fixed steps (52 for weekly), and steps per year for calendar frequencies (12 for monthly). A candidate below 2 means no period. - A frequency-derived period is accepted when the series spans at least 3 cycles and the lag-one-period autocorrelation (over pairs observed at both ends) is at least 0.3.
- A period given with
seasonal_periodonly needs the series to span 2 cycles.
Per-gap imputation
Each gap gets its own method (impute_method: "auto"):
| Condition | Method | reason |
|---|---|---|
| Gap ≤ 3 steps, with at least 2 observations before and after it | pchip | short_gap |
| Gap ≤ 3 steps, but fewer than 2 observations on one side | linear | short_gap_few_neighbours |
| Longer gap, seasonal period accepted, and every missing phase has at least 2 same-phase observations within 7 cycles on either side | seasonal | seasonal_period_accepted |
| Longer gap, seasonal period accepted, but not enough same-phase history | linear | insufficient_history |
| Longer gap, seasonal period not accepted | linear | no_seasonality:<reason>, with the seasonality.reason |
pchip: Fritsch–Carlson monotone cubic interpolation through all observations; each segment stays between its two end observations, so fills never overshoot.seasonal: the median of the same phase over the observations within 7 cycles on either side, plus the offsets of the gap's two neighbouring observations from their own medians, blended linearly across the gap so the fill joins both ends.- A forced
linearorpchipis used for every gap (reason: method_option). A forcedseasonalneeds a period spanning at least 2 cycles (autocorrelation is not required) and enough same-phase history; otherwise it falls back to linear withreasonseasonal_unavailable(no usable period) orinsufficient_history. - The
tierthresholds are those of section 4.14, with P taken fromseasonality.period; without a period, every gap longer than 3 steps isT1. - When gaps use different methods, the
gap.imputeaction ismixedandevidence.methodscounts the points filled by each method.
Physical constraints on filled values
Constraints act only on filled values; observed values are never changed. They run in this order:
- Structural zeros (
constraint.structural_zero, actionzero). A seasonal phase observed in at least 7 cycles and zero in at least 90% of them (for example solar output at night) keeps its filled values at 0. The series must span at least 7 cycles; if every phase qualifies the series is constant and nothing is done.structural_zero: "off"turns it off. - Non-negativity (
constraint.nonnegative, actionclip_at_zero). See thenonnegativeparameter in section 3. - Bounds (
constraint.bounds, actionclip_to_bounds). Clips into the declaredbounds.
Each constraint that actually changes a filled value produces a decision (class fabricate, reversible: true) that can be declined by id. The gaps it touched list the rule in gaps[].constraints, and actions[imputed].constraints counts the points each rule changed. Observed values breaking a constraint are only flagged: negative_in_nonnegative (negative observations with nonnegative: "true", or under auto when at least 99% of observations are non-negative and only a few are negative, in which case nothing is clipped) and out_of_bounds.
Real output: hourly solar data over 8 days, day 6 at half output, 14 points missing from 2026-06-06T16:00Z to 2026-06-07T05:00Z (overnight):
"seasonality": {"period": 24, "source": "frequency", "acf": 0.9307, "cycles": 8.0,
"accepted": true, "reason": "accepted"}
"gaps": [{"range": {"start": "2026-06-06T16:00:00Z", "end": "2026-06-07T05:00:00Z", "points": 14},
"method": "seasonal", "tier": "T1", "reason": "seasonal_period_accepted",
"constraints": ["constraint.structural_zero", "constraint.nonnegative"]}]
{"id": "constraint.structural_zero:pv-01", "rule": "constraint.structural_zero",
"class": "fabricate", "status": "applied", "action": "zero",
"before": {"imputed_points": 14}, "after": {"changed": 12}, "reversible": true,
"evidence": {"period": 24, "phases": [0, 1, 2, 3, 4, 5, 6, 18, 19, 20, 21, 22, 23],
"zero_share": 0.9, "min_cycles": 7}}
{"id": "constraint.nonnegative:pv-01", "rule": "constraint.nonnegative",
"class": "fabricate", "status": "applied", "action": "clip_at_zero",
"before": {"imputed_points": 14}, "after": {"changed": 1}, "reversible": true,
"evidence": {"source": "auto"}}
// fills: 17.1, then 13 points at 0 (night stays at 0)Under default@2026-10 the same 14 points are filled linearly (tier: T1, reason: no_seasonal_method), and the night-time fills are above 0.
Fixed forecast origin (2026-12)
2026-12 keeps every 2026-11 rule above and changes one thing in step 4.7: the forecast always starts one step after the last ds you send, even when the last y values are null (for example an interval whose value has not arrived yet).
- Trailing nulls after the last observation, up to 3 grid steps (the short-gap limit), stay on the grid instead of being trimmed (decision
gap.masked_tail, classfabricate, actionmask_for_model; issue and warningmasked_tail; actionmaskedwithreason: trailing_null). They are not used as data: they do not count infed_points, are not billed, and the entity'smodel_input[]entry reports them asmasked_tailwith atrailing_values_missingentry in the responsewarnings[]. - A longer trailing run, or declining
gap.masked_tail:<unique_id>, fails withSERIES_DISCONTINUOUS(blockingtrailing_gap_too_long, withcountandmax_steps) rather than producing a forecast that starts earlier than you asked. Send the newest observed values or drop the trailing empty rows. - Under
strict,gap.masked_tailis demoted to a proposal like other non-structural decisions; accept it byid. - Leading nulls are still trimmed as in step 4.7. Under
2026-10and2026-11trailing nulls are trimmed too (edge_null) and the forecast starts after the last non-nully.
Missing-data rule (2026-13)
2026-13 keeps every 2026-12 rule and replaces the missing-share check of step 4.10. Earlier versions refuse any series with 10% or more missing; 2026-13 looks at where values are missing instead, because a long gap just before the forecast start hurts the forecast far more than the same number of scattered missing values. Gap lengths are measured in seasonal periods of the series: seasonal_period when you pass it, otherwise 24 steps for hourly data, 48 for 30-minute, 96 for 15-minute, 7 for daily, 52 for weekly and 12 for monthly. Trailing nulls kept for the model (masked_tail) do not count.
| Check | Refused (SERIES_DISCONTINUOUS) | Accepted with warning missing_pattern |
|---|---|---|
| A gap followed by fewer than 0.5 periods of values (a recent gap) | 0.75 periods or longer (reason: gap_near_origin_too_long) | 0.25 periods or longer (gap_near_origin) |
| Any other gap | never refused on its own | 2 periods or longer (long_gap) |
| Missing share (gaps plus nulls) | 35% or more (reason: missing_share_too_high) | 20% or more (missing_share_high) |
Without a seasonal period (for example an irregular frequency) only the share is checked. A refused entity keeps blocking code health_critical and adds reason, reasons, period, near_gap_steps and far_gap_steps; caused_by works as before. Send the missing recent values, or only the history before the gap.
health.factors lists what the health score deducted, as {code, points, count?, ratio?} with the points summing to 100 − score: gap_near_origin, far_gap, missing_share (together at most 49 points on an accepted entity), conflicting_duplicates, outliers (at least 1 point per flagged entity), flat_runs, level_shifts, and blocked (keeps a refused entity at 49 or below). Tier follows the table: refused is Critical, a warning is Warning.
Change from 2026-12. Some series that 2026-12 refused because 10% or more was missing are now accepted (with the warning missing_pattern when 20% or more is missing), and some series with a long gap just before the forecast start, which 2026-12 accepted while less than 10% was missing, are now refused with reason: gap_near_origin_too_long. To keep the old behaviour for a while, pin options.cleaning.version: "2026-12" (deprecated).
6. The strict preset
strict runs every rule of default for the same version, then keeps only structural decisions: every other applied decision becomes proposed, and the request fails with 422 and the report:
| Still applied | Demoted to proposals |
|---|---|
time.reorder, time.duplicates with identical values, grid.snap, grid.regrid, grid.business_days (2026-11 and later). window.context is not applied by the API: an over-long entity returns SERIES_TOO_LONG (see §4.8) | gap.impute, gap.edge_trim, gap.split, grid.off_grid, conflicting time.duplicates, value.sentinel, value.tokens, value.dash_token, constraint.* (2026-11 and later), gap.masked_tail (2026-12) |
- If a demoted decision is a
time.*orgrid.*rule the request fails withTIMESTAMP_IRREGULAR, otherwise withSERIES_DISCONTINUOUS(blockingcleaning_required). - A decision
idlisted inacceptis applied as usual; once every demoted decision is accepted the request goes through. - Under
strictthe report'sactionsstill describe whatdefaultwould have done; thedecisionsstatuses are authoritative.
7. The raw preset
raw@2026-10 does not clean: no sorting, de-duplication, filling or time-zone inference, only a structural check before the data goes to the model as sent. freq is required and must be a fixed step; each ds must be exactly YYYY-MM-DDTHH:MM[:SS[.ffffff]] followed by Z or ±HH:MM; each entity must be strictly increasing at a step equal to freq; every y must be finite. Otherwise the request fails with RAW_INPUT_INVALID. The response has no cleaning_report, and accept / decline and cleaning parameters are not accepted. Details are in the raw preset.
8. Reading the result: effective_policy, decisions, accept and decline
effective_policy.idis the policy that actually ran;cleaning_report.policyholds every parameter the cleaning service resolved (short-gap limitgap_fill_max_steps: 3, outlier thresholdoutlier_threshold: 3.5, duplicate handlingduplicate_policy: "last"and so on), plus theaccept/declineyou sent.cleaning_report.decisions[]lists every data-changing decision and every proposal that was not applied. Theidformat isrule:unique_id;value.sentineladds the value as a selector, for examplevalue.sentinel:meter-001:-9999.status:appliedchanged the data;proposedleft it untouched;declinedwas reverted by you.acceptapplies a proposal:value.sentinel,value.dash_token(read as 0),gap.split, and any decision demoted bystrict.declinereverts a decision withreversible: true:value.sentinel,gap.split,grid.business_days,constraint.structural_zero,constraint.nonnegative,constraint.bounds. Declininggap.masked_taildoes not trim the tail; it fails the request withtrailing_gap_too_long.- Do not list the same
idin both. Anidthat matches nothing in this data adds adecision_not_applicablewarning and the request proceeds. issues[]are findings (with positions),warnings[]are non-blocking notices,blocking[]are the reasons for a rejection. When a request is rejected, the error code follows the first blocking reason.
Request examples and before/after for each rule are in Cleaning Presets & Report.
9. Glossary
Decision rules
| Rule | Class | Action | Meaning | Accept / decline |
|---|---|---|---|---|
time.reorder | structural | stable_sort | Rows were out of time order and have been stably sorted. | — |
time.duplicates | structural / drop | keep_last | Several rows share a timestamp; only the last is kept. drop when their values differ. | — |
grid.snap | structural | nearest_slot | Stamps slightly off the grid were aligned to the nearest slot. | — |
grid.off_grid | drop | drop | Stamps between two slots; those rows were dropped. | — |
grid.business_days | structural | business_day_grid | 2026-11. Weekday-only daily data moved to the business-day grid B; weekends are not gaps. | Declinable |
gap.edge_trim | drop | drop | Nulls at the start and end of the series were dropped. | — |
gap.masked_tail | fabricate | mask_for_model | 2026-12. Up to 3 trailing nulls stay on the grid and are not used as data, so the forecast still starts after the last ds. | Declinable (then the request fails with trailing_gap_too_long) |
window.context | drop | keep_latest | History longer than the model's maximum context; only the most recent part is kept. Not produced by the API, which returns SERIES_TOO_LONG instead. | — |
gap.split | drop | keep_latest_segment | A very long gap; only the latest segment after it is kept. | Acceptable, declinable |
grid.regrid | structural | insert_empty_slots | Stamps missing from the grid were inserted as empty slots. | — |
gap.impute | fabricate | linear / pchip / seasonal / mixed | Missing values were filled; the action names the method (2026-10 is always linear). | — |
constraint.structural_zero | fabricate | zero | 2026-11. Filled values in phases that are almost always 0 were set to 0. | Declinable |
constraint.nonnegative | fabricate | clip_at_zero | 2026-11. Filled values of a non-negative series were clipped at 0. | Declinable |
constraint.bounds | fabricate | clip_to_bounds | 2026-11. Filled values were clipped into the declared range. | Declinable |
value.sentinel | fabricate | null | A common missing-value code was nulled and then filled; only proposed when the evidence is weak. | Acceptable, declinable |
value.tokens | semantic | null | Missing-value tokens in text cells (such as N/A) were nulled. | — |
value.dash_token | semantic | zero | A lone dash; reading it as 0 is proposed, otherwise it is missing. | Acceptable |
Every decision demoted by strict can be accepted.
Issue codes issues[].code
| Code | Meaning |
|---|---|
timestamp_format | Timestamps are not ISO 8601, or the day/month order was inferred from the data; the report names the format used. |
naive_timestamps_assumed_utc | Timestamps carry no offset and options.timezone was not set; they were read as UTC. |
dst_missing | The hour skipped when daylight saving starts (from an offset change, or a guess for UTC data). |
dst_duplicate | The hour repeated when daylight saving ends. |
dst_nonexistent_time | The local time does not exist in the zone used (skipped by daylight saving). |
mixed_timezone | The UTC offset changes within one entity by something other than daylight saving; zones may be mixed. |
non_numeric_token | Text cells are missing-value tokens (such as N/A) and were nulled. |
dash_token | Text cells are a lone dash, which may mean 0 or missing. |
sentinel_value | Missing-value codes were nulled. |
suspected_sentinel | Possible missing-value codes without enough evidence; values kept. Accept by id to null them. |
thousands_separator | 2026-11. Numbers written with thousands separators; left missing. |
decimal_comma | 2026-11. Numbers written with a decimal comma; left missing. |
ambiguous_separator | 2026-11. A comma that may be a thousands separator or a decimal point; left missing. |
number_format_mixed | 2026-11. Thousands separators and decimal commas mixed in one series. |
non_numeric_value | 2026-11. Text that is not a number (for example with a unit); left missing. |
missing_value | y is null. |
business_days_pattern | 2026-11. Weekday-only data, now on the business-day grid; count is the weekend days skipped. |
business_days_not_applicable | 2026-11. The business-day grid was requested but the data is not daily, so it was not used. |
out_of_order | Rows were out of time order. |
duplicate_timestamp | Several rows share a timestamp; conflicting counts duplicates with different values. |
entity_column_suspected | Many conflicting duplicate timestamps; several series may be mixed under one unique_id. |
timestamp_jitter | Stamps slightly off the grid, aligned. |
off_grid | Stamps between two slots, dropped; ratio is their share. |
edge_null | Nulls at the start and end of the series, dropped. |
masked_tail | 2026-12. Trailing nulls kept on the grid and not used as data; count is the number of steps. |
trailing_gap_too_long | 2026-12. More trailing nulls than can be kept (max_steps), or gap.masked_tail was declined (declined: true). |
missing_pattern | 2026-13. Where values are missing would hurt the forecast; reasons lists the checks hit (see Missing-data rule). Severity warning when accepted, error when refused. |
context_window | Reserved; not produced by the API, which rejects such series with SERIES_TOO_LONG. |
segment_split_skipped | A very long gap, but splitting would drop over half the observations, so the gap was filled instead. Accept the split by id to force it. |
gap | Stamps missing from the grid. |
long_gap | A run of missing values longer than the short-gap limit (3 steps), still filled. |
outlier | Outliers, flagged only; values unchanged. |
constant_series | 2026-11. Observations never change, so there is no signal to forecast from. |
all_zero_series | 2026-11. All observations are 0, which often means no data. |
flat_run | A run of identical non-zero values, possibly a stuck sensor; flagged only. |
level_shift | Two stretches differ by a factor of about 1000, possibly a unit mix-up; flagged only. |
negative_in_nonnegative | 2026-11. Negative observations in a non-negative series; flagged only. |
out_of_bounds | 2026-11. Observations outside the declared bounds; flagged only. |
too_short | Fewer points than the model's minimum context after cleaning, or no values at all. |
Warnings warnings[].code
| Code | Meaning |
|---|---|
freq_low_confidence | The inferred frequency has 70% to 90% confidence; it is used, but passing freq explicitly is recommended. |
duplicate_conflict | Duplicate timestamps had different values; the last row was kept. |
entity_column_suspected | Summary of the issue of the same name. |
off_grid_dropped | Off-grid rows were dropped. |
segment_split | The series was split at a long gap and earlier data dropped. |
segment_split_skipped | Summary of the issue of the same name. |
dst_suspected | Gaps or repeats in UTC data that look like daylight saving; consider passing options.timezone. |
long_gap_filled | A gap longer than the short-gap limit was filled. |
masked_tail | 2026-12. Summary of the issue of the same name. |
missing_pattern | 2026-13. Summary of the issue of the same name, with reasons. |
outlier_flagged_not_modified | Outliers were flagged; values unchanged. |
decision_not_applicable | ids in accept / decline that match nothing in this data; ids lists them. |
Blocking reasons blocking[].code
| Code | Error code | Meaning |
|---|---|---|
unparseable_timestamp | VALIDATION_ERROR | 2026-11. Timestamps that cannot be parsed; rows lists where. |
ambiguous_date_format | AMBIGUOUS_DATE_FORMAT | The day/month order cannot be proven from the data. |
freq_irregular | FREQ_INFERENCE_FAILED | No stable frequency could be inferred, or more than 10% of rows are off-grid (reason: off_grid). |
freq_ambiguous | FREQ_AMBIGUOUS | Several candidate frequencies, or entities inferring different ones. |
freq_unsupported | FREQ_UNSUPPORTED | The frequency is not recognized. |
all_null | SERIES_EMPTY | The entity has no numeric y. |
health_critical | SERIES_DISCONTINUOUS | A recent gap or the missing share failed the missing-data rule, named in reason (deprecated versions: the missing share reached 10%). With caused_by, the missing values come mainly from missing-value decisions; adjust with the decline / accept it lists. |
too_short_for_model | SERIES_TOO_SHORT | Fewer points than the model's minimum context after cleaning. |
trailing_gap_too_long | SERIES_DISCONTINUOUS | 2026-12. More trailing nulls than can be kept, or gap.masked_tail was declined; count and max_steps give the numbers. |
cleaning_required | TIMESTAMP_IRREGULAR / SERIES_DISCONTINUOUS | Repair needed, but clean_series is false or strict proposals were not accepted; reason is timestamp_irregular or series_discontinuous. |
Actions actions[].code
| Code | method | Meaning |
|---|---|---|
reordered | stable_sort | Rows re-sorted by time. |
deduplicated | last | Duplicate timestamps dropped, last row kept. |
snapped | nearest_slot | Stamps aligned to slots. |
dropped | grid_align / edge_trim / latest_segment | Data dropped; reason is off_grid, edge_null or long_gap_split. |
regridded | insert_missing_stamps | Missing stamps inserted. |
sentinel_to_null | token_table / sentinel_list | Missing-value tokens or codes nulled. |
imputed | linear / pchip / seasonal / mixed | Missing values filled; gaps[] gives each gap's range, method, tier and reason. |
masked | model_mask | 2026-12. Trailing nulls kept for the model and not used as data; reason is trailing_null. |
Imputation reasons gaps[].reason
| Reason | Version | Meaning |
|---|---|---|
within_gap_fill_max_steps | 2026-10 | The gap is within the short-gap limit (3 steps). |
no_seasonal_method | 2026-10 | The gap exceeds the short-gap limit; this version has no seasonal fill, so it is linear. |
exceeds_long_gap_max | 2026-10 | The gap is longer than the T2 limit. |
short_gap | 2026-11 | Short gap with enough observations on both sides; PCHIP. |
short_gap_few_neighbours | 2026-11 | Short gap with fewer than 2 observations on one side; linear. |
seasonal_period_accepted | 2026-11 | The seasonal period was accepted; seasonal fill. |
no_seasonality:<reason> | 2026-11 | The seasonal period was not accepted; linear. Reasons are in the next table. |
insufficient_history | 2026-11 | Not enough same-phase history; fell back to linear. |
method_option | 2026-11 | The method forced by impute_method. |
seasonal_unavailable | 2026-11 | seasonal was forced but no usable period exists; fell back to linear. |
Seasonality verdicts seasonality.reason
| Reason | Meaning |
|---|---|
accepted | The frequency-derived period passed (at least 3 cycles, autocorrelation at least 0.3). |
option | The period given with seasonal_period (at least 2 cycles). |
insufficient_cycles | The series spans too few cycles. |
weak_autocorrelation | The lag-one-period autocorrelation is below 0.3 or cannot be computed. |
no_seasonal_period | The frequency has no candidate period. |
Gap tiers gaps[].tier
| Tier | Meaning |
|---|---|
T0 | Short gap: within the short-gap limit (3 steps). |
T1 | Up to one seasonal cycle. |
T2 | Up to 3 seasonal cycles or 5% of the series length, whichever is larger. |
T3 | Longer. |