Diagnose questionnaire or contract fields where a derived date (end date, expiration, renewal) is correct on first fill but does not update after the user edits upstream fields (effective date, term length, duration). Covers (1) Liquid template patterns: stale assigns when conditionals skip, self-referencing consecutive assigns, broken | date format strings; (2) platform: QuestionnaireFormService mergeBlanks recursively merging DateObject-shaped values into invalid/empty objects (fixed angular-frontend#15092, SPD-42840). Use when: 'end date not updating', 'expiration date wrong after changing term', 'sd_expiration_date stuck', 'effective date changed but end date old', 'NDA term length edit does not refresh end date'. Prefer this over contract-computation-null-propagation when the field had a value and edits to other fields do not refresh it (not blank-from-null-operands). Known example: Dream11 NDA TPP (Rootly #3052, SPD-42840, WSID 456675, Prod IN, Mar–Apr 2026).
A date field derived from other questionnaire inputs (e.g. sd_expiration_date from sd_effective_date + sd_term_length) behaves correctly on initial evaluation but stays wrong or stale after the customer or legal team changes an upstream field.
Primary mechanisms (often combinable):
Stale value when the main {% if %} guard fails after an edit
If the derived field is only assigned inside a conditional and there is no {% else %} that clears it, a later edit can leave the previous assign in blanks — Liquid does not implicitly delete keys when the block is skipped.
Self-referencing consecutive assigns
Patterns like {% assign sd_expiration_date = sd_effective_date | plus: sd_term_length %} immediately followed by {% assign sd_expiration_date = sd_expiration_date | minus: one_day %} can interact badly with evaluation order / intermediate blanks in some paths; using a for the first result, then assigning the final slug once, avoids read-after-write ambiguity.
Typo in | date: format string
Example from production triage: "%Y-%m -%d" (space before -%d) produced strings like 2025-03 -26 instead of 2025-03-26, breaking string comparisons to "now" | date: \"%Y-%m-%d\"** (e.g. sd_dummy-style gates).
Missing explicit dependencies (product recommendation)
Relying only on raw Liquid assign in template computation without treating the field as a computed field wired to dependencies (sd_effective_date, sd_term_length, etc.) makes it easier to hit partial-update paths where the template author expected a full re-eval that does not match runtime behavior. Preferred fix class: model the derived date as a computed field with formula effective_date + term_length - 1 day (or equivalent) so dependency changes trigger recomputation explicitly.
mergeBlanks treating date values like plain objects (platform — SPD-42840)
QuestionnaireFormService.mergeBlanks merges nested objects recursively. Date values in blanks are not normal POJOs — deep-merge paths could produce an empty or invalid object and break derived dates after edits even when Liquid output is correct. Fix: angular-frontend#15092 — if either side of a key is a date object (isDate), replace the value instead of merging. Same Jira as Dream11 (SPD-42840); triage template and frontend together for that incident class.
Platform note: The questionnaire does re-run computation on value changes via #runComputation → Liquid then Specter. For “derived date wrong after edit”, consider both template/Liquid issues (1–4) and whether the app build includes #15092 for (5) — do not assume template-only root cause when the symptom matches SPD-42840.
Distinguish from:
| Skill | Why different |
|---|---|
| contract-computation-null-propagation | Blank field from plus/minus/times with null/missing operands (often conditional questions never shown). Here the derived date was set and fails to refresh after edits. |
| conditional-default-currency-questionnaire-triage | Currency typing, linked dropdown, times on money — not date derivation from term length. |
Run BQ queries one at a time — parallel
execute_sqlcalls often return 500 errors.
To confirm persisted blanks after repro (adjust dataset per cluster — same mapping as contract-computation-null-propagation):
SELECT id, source, is_completed, data, modified
FROM `spotdraft-prod.prod_india_db.public_contracts_v3_contractdata`
WHERE contract_id = {contract_id}
ORDER BY modified DESC
LIMIT 5
Look for sd_expiration_date (or the customer’s slug) unchanged in data while sd_effective_date / sd_term_length did change — supports stale-assign / conditional-skip theory.
Use when:
assign + plus/minus on duration objects and date filters for comparisons.Do not use when:
times on money → conditional-default-currency-questionnaire-triage.From Slack or the ticket:
{workspace_id}, {cluster}, {contract_id} (if any), {template_id} / workflow manager linksd_effective_date, sd_term_length, sd_expiration_date)Example (Dream11, Mar 2026): WSID 456675, cluster IN, workflow manager path referenced in incident: .../workflow-manager/8516/details.
If it does not update, continue — triage template computation and frontend merge behavior (Step 3 and Step 3b).
Pull the full template computation (Django Admin questionnaire / Metabase by template_id — URLs as in contract-computation-null-propagation).
Checklist on the Liquid:
{% if ... %} with no {% else %} that sets the slug to nil when guards fail?{% assign same_slug = ... %} lines in a row where the second reads the slug assigned on the previous line? Try temp variable pattern (see Mitigation).date: " and verify no stray spaces in format strings (must be "%Y-%m-%d", not "%Y-%m -%d").If the ticket references SPD-42840 or the same symptom as Dream11 (NDA TPP / effective + term → end date):
mergeBlanks: replace-only when source or existing value is a date object).| Finding | Interpretation |
|---|---|
Adding {% else %}{% assign derived = nil %}{% endif %} fixes stale value | Stale conditional path confirmed |
Fixing date: format fixes comparison / dummy flag behavior | Format typo path |
| Temp variable before final assign fixes inconsistency | Self-reference assign order |
| Computed field with deps fixes without fragile Liquid | Preferred long-term modeling |
| Repro on old FE, gone after #15092 deploy, template already sane | mergeBlanks / DateObject path (platform) |
| Latest FE + good template still broken | Escalate with both computation text and blanks snapshots after each field change |
mergeBlanks (questionnaire-form.service.ts).if with {% else %} assigning nil to the derived slug when prerequisites are missing.temp_expiration (or similar) for intermediate plus/minus, then one assign into the customer-visible slug."%Y-%m-%d" consistently for string compares against now.effective_date + term_length - 1 day or product-equivalent).#runComputation), or if post-#15092 repro still shows corrupt date objects in blanks after merge.Illustrative only; confirm against your template’s types and tags:
{% if sd_effective_date and sd_term_length and sd_term_length.value and sd_term_length.type %}
{% parseAssign one_day = '{"value": 1, "type": "DAYS", "days": 1}' %}
{% assign temp_expiration = sd_effective_date | plus: sd_term_length %}
{% assign sd_expiration_date = temp_expiration | minus: one_day %}
{% else %}
{% assign sd_expiration_date = nil %}
{% endif %}
{% if sd_effective_date %}
{% assign parse_sd_effective_date = sd_effective_date | date: "%Y-%m-%d" %}
{% assign current_date = "now" | date: "%Y-%m-%d" %}
...
{% endif %}
Do not copy blindly: variable names, duration shape, and additional blocks (sd_date_of_request, etc.) must match the live template.
libs/questionnaire/questionnaire-form/src/lib/questionnaire-form.service.ts — #runComputation, #getValuesPostComputation.mergeBlanks. Used when combining persisted vs unsaved values and computation output; date objects must be replace-only so derived dates stay valid after dependency edits.libs/liquid-ng/src/lib/utils.ts — getBlanksAsObservable / parseAndRender mutates scope after render.libs/specter-langium/src/lib/specter-evaluator.ts — evaluateSpecter skips when dependencies missing (returns null); relevant when migrating from Liquid to computed fields.Use these when correlating “does the client re-run computation?” and “are date fields intact after merge?” with observed behavior; they complement template correctness.
| Item | Detail |
|---|---|
| Rootly | #3052 — Dream11 end date calculation |
| Jira | SPD-42840 |
| Frontend fix | angular-frontend#15092 — mergeBlanks replace-only for date objects (closes SPD-42840) |
| Example context | WSID 456675, cluster IN, Mar–Apr 2026; mitigation: template Liquid fixes + #15092 on questionnaire; structural recommendation: computed field with dependencies |