Docassemble Interview Logic
Docassemble Interview Logic
Mental model: a checklist, not a flowchart
Every time a screen loads, docassemble re-evaluates the interview from the top. It runs mandatory and initial blocks in YAML order, and whenever it hits an undefined variable it pauses, finds and runs the block that defines that variable (asking the user a question or running a code block), then restarts from the beginning. This is dependency satisfaction.
Think of it as a checklist that is re-run on every screen load, not a linear sequence of steps. The “current question” is whatever the checklist discovers is still missing.
mandatory — the entry point
All blocks are optional unless marked mandatory: True. Docassemble runs mandatory blocks in the order they appear in the YAML file. An already-completed mandatory block is skipped on subsequent runs.
mandatory can also be a Python expression; the block is treated as mandatory only when that expression is truthy:
mandatory: favorite_fruit == 'apple'
question: What type of apple is your favorite?
fields:
- Type: favorite_apple_type
Best practice: Use a single mandatory code block as an explicit outline of the interview, then let dependency satisfaction handle gathering variables:
mandatory: True
code: |
user.name.first
if user.age_in_years() < 18:
parental_consent
final_screen
Each bare variable reference means “if this variable is undefined, stop and define it; otherwise continue.” Tag every mandatory block with an id to avoid re-asking questions if you rearrange the YAML later.
initial — runs on every screen load
An initial: True code block runs every time the screen loads, even after it has completed successfully. Use it to initialize session context (e.g., setting user based on the logged-in user). Do not use initial for anything that should only happen once.
objects block — declaring objects
Use objects to initialize docassemble objects. This is the standard way to declare DAList, Individual, Address, and other objects. The block is processed when docassemble needs to define the listed variable:
objects:
- user: Individual
- children: DAList.using(object_type=Individual, complete_attribute='complete')
- address: Address
All objects used as interview variables must inherit from DAObject. Use the .using() method to set initial attributes inline:
objects:
- fruit: DAList.using(object_type=str, minimum_number=1, ask_number=True)
Prefer objects blocks over code blocks for initialization — docassemble needs the full variable name to be passed correctly, and objects handles that automatically.
File organization: include and modules
include
include:
- basic-questions.yml
- docassemble.helloworld:questions.yml
include incorporates another YAML file’s blocks at compile time, as if they appeared in place of the include block. Modifiers like if and mandatory have no effect on include — it cannot be made conditional. To reference a file from another package: packagename:filename.yml.
Because included blocks appear before the blocks in the including file, they have lower override priority (later = higher priority).
imports and modules
imports:
- datetime # equivalent to: import datetime
modules:
- .utils # equivalent to: from .utils import *
Use imports for standard library or third-party modules you access by module name. Use modules for your own utility modules that export functions and classes. Define __all__ in your module files to limit what modules imports.
Dependency satisfaction in practice
When docassemble needs a variable it searches for the best block to define it, then runs that block. If the block itself needs another undefined variable, the search recurses. Non-mandatory blocks can appear in any order; their position relative to each other only matters when multiple blocks can define the same variable (see Overriding below).
Example — the mandatory block asks for final_screen; docassemble automatically discovers and asks everything final_screen refers to:
mandatory: True
event: final_screen
question: |
Your favorite fruit is ${ favorite_fruit }.
% if favorite_fruit == 'grapes':
You prefer ${ favorite_vineyard } grapes.
% endif
---
question: What is your favorite fruit?
fields:
- Fruit: favorite_fruit
---
question: Which vineyard is your favorite?
fields:
- Vineyard: favorite_vineyard
If the user answers “apples,” favorite_vineyard is never asked because it is never referenced in a code path that runs.
Idempotency — the most common pitfall
Because code blocks can be interrupted and restarted whenever an undefined variable is encountered, a block may execute multiple times before it completes. Code that accumulates a value will double-count:
# BAD — total_income is incremented each time this block restarts
mandatory: True
code: |
total_income = total_income + benefits_income # restarts here if benefits_income undefined
total_income = total_income + net_business_income
Fix: initialise and compute atomically so re-running produces the same result:
# GOOD
mandatory: True
code: |
total_income = 0
total_income += benefits_income
total_income += net_business_income
Or better, keep calculation code in a separate non-mandatory block that assigns the variable in a single expression:
code: |
total_income = benefits_income + net_business_income
Similarly, side-effects (sending emails, storing data) must be guarded so they fire only once:
mandatory: True
code: |
user.email
if not task_performed('welcome_email'):
send_email(to=user, subject="Welcome", body=body_text, task='welcome_email')
final_screen
Conditional branching
In a mandatory code block
Use normal Python if / elif / else. Referencing a variable that is undefined causes docassemble to seek its definition:
mandatory: True
code: |
if user.age_in_years() >= 60 or user.is_disabled:
eligible_screen
else:
ineligible_screen
Docassemble short-circuits: if user.age_in_years() >= 60 is true, user.is_disabled is never asked.
Branching with event screens
Terminal screens (no Continue button) use event:
event: ineligible_screen
question: Sorry, you do not qualify.
Referencing ineligible_screen in a code block causes docassemble to display that screen and stop.
Overriding blocks
When multiple blocks can define the same variable, docassemble tries later-defined blocks first. This lets you override a question from an included library without editing that library:
include:
- question-library.yml # defines user_wants_to_go_to_dance
---
# This later block supersedes the library question
question: Hey, want to dance?
yesno: user_wants_to_go_to_dance
Key block-level specifiers
need
Force docassemble to gather specific variables before processing a block, even if those variables are not mentioned in the block’s content:
need:
- favorite_fruit
question: Your favorite apple is ${ favorite_apple }.
continue button field: fruit_verified
Use post to gather a variable after the block’s own prerequisites:
need:
- favorite_vegetable
- post:
- favorite_fruit
depends on
When listed variables change (e.g., via a review screen), invalidate this block’s output so it is recomputed:
depends on:
- a
code: |
b = a * 2
If the user later edits a, b is automatically undefined and recomputed.
reconsider
reconsider: Trueon acodeblock: forget the variables it sets on every screen load, forcing recomputation. Use when a computed variable must reflect information gathered after it was first computed.reconsider: [var1, var2]on aquestion: undefine those variables before showing the question, so they are looked up fresh.
reconsider: True
code: |
cat_food_cans_needed = number_of_cats * 4
undefine
Undefine listed variables before showing a question (useful in review screens):
undefine:
- favorite_foods
question: What is your favorite fruit?
fields:
- Favorite fruit: favorite_fruit
only sets
Tell docassemble that this code block should only be consulted to define a specific variable, even though it may set others as side effects:
only sets: total_income
code: |
temp = []
for item in income:
if item.included:
temp.append(item.value)
if item.type == 'disability':
has_benefits = True # side effect, but not the purpose of this block
total_income = sum(temp)
Block search order summary
| Direction | Purpose |
|---|---|
| Top → bottom | Which mandatory/initial blocks to run, and in what order |
| Bottom → top | Which non-mandatory block to use to define a needed variable (later = higher priority) |
Common patterns
Explicit interview outline
mandatory: True
code: |
intro_screen
user.name.first
if applicant_is_eligible:
document_screen
else:
rejection_screen
One-time side effect
mandatory: True
code: |
user.email
email_sent # triggers the code block below exactly once
final_screen
---
code: |
send_email(to=user, subject="Welcome", body="...")
email_sent = True
Encoding legal rules as code
Write eligibility rules as Python, then let dependency satisfaction ask only the questions needed:
code: |
if relationship == 'Grandparent' \
and willing_to_assume_responsibility \
and (child_is_dependent or child_is_at_risk):
has_standing = True
else:
has_standing = False
---
mandatory: True
code: |
if has_standing:
final_screen
else:
no_standing_screen
Docassemble will not ask child_is_at_risk if willing_to_assume_responsibility is False.
reset and on change
reset — unconditional recomputation on every screen load
reset:
- client_is_guilty
Variables listed in a reset block are undefined on every screen load, forcing recomputation. This is equivalent to applying reconsider: True to every code block that defines those variables. Use sparingly — it adds overhead on every page load.
on change — react when a variable’s value changes
on change:
married: |
income.reset_gathered(mark_incomplete=True)
undefine('income[i].amount')
The code runs when married changes value, and also when it is first initialized. Use on change when depends on is insufficient — for example, when you need to invalidate indexed attributes like income[i].amount where the index variable i is not in scope at change time.
on change code must run to completion without encountering any undefined variables. It runs before modules and imports blocks have loaded; standard docassemble utility functions (undefine(), invalidate(), etc.) are available.
Groups: DAList, DADict, DASet
Docassemble provides three collection types that integrate with the dependency-satisfaction engine:
DAList— ordered list, elements accessed by integer indexDADict— key/value mapping, elements accessed by keyDASet— unordered set of unique items
Use objects blocks (not plain Python list, dict, or set) to declare these so docassemble can gather them automatically.
Gathering a DAList
objects:
- fruit: DAList
The gathering algorithm asks three types of questions in a loop:
fruit.there_are_any— “Is there at least one item?”fruit[i]— “What is the item at indexi?”fruit.there_is_another— “Are there more items?”
After each item is added, docassemble undefines fruit.there_is_another and re-seeks it. When the user answers no, gathering is complete.
Write one question for fruit[i] using the special index variable i:
question: Are there any fruits to add?
yesno: fruit.there_are_any
---
question: What fruit should be added?
fields:
- Fruit: fruit[i]
---
question: So far the fruits include ${ fruit }. Are there others?
yesno: fruit.there_is_another
The index variable i is a special variable set automatically by docassemble. Never set i yourself. Never use i in mandatory or initial blocks.
Gathering is triggered implicitly when you iterate the list, call .number() or .number_as_word(), or include ${ fruit } in a template. Trigger it explicitly with fruit.gather().
complete_attribute — gathering all attributes per item
When list items are objects with multiple attributes, set complete_attribute='complete' so docassemble finishes gathering every attribute for one item before moving on to the next:
objects:
- friend: DAList.using(object_type=Individual, complete_attribute='complete')
---
code: |
friend[i].name.first
friend[i].birthdate
friend[i].favorite_animal
friend[i].complete = True
This code block runs for each friend[i], gathering all its attributes before the list moves to the next item. When the user later edits a list item via a table, docassemble undefines .complete and re-runs this block.
Nested lists: index variables i and j
For a list within a list, use i for the outer index and j for the inner index:
objects:
- person: DAList.using(object_type=Individual, complete_attribute='complete')
- person[i].child: DAList.using(object_type=Individual, complete_attribute='complete')
---
# defines person[i].complete — uses i only
code: |
person[i].name.first
person[i].child.gather()
person[i].complete = True
---
# defines person[i].child[j].complete — uses both i and j
code: |
person[i].child[j].name.first
person[i].child[j].complete = True
Keep the two code blocks separate. If you mix i and j in the same block, docassemble will not set j when it is seeking person[i].complete, causing an error.
The index hierarchy is always i → j → k. A block that uses j must also reference i; a block that uses k must reference both j and i.
generic object — reusable blocks across instances
Use generic object: ClassName with the special variable x to write one block that applies to any instance of that class, regardless of its nesting depth:
generic object: Individual
objects:
- x.allergy: DAList
---
generic object: Individual
question: Does ${ x } have any allergies?
yesno: x.allergy.there_are_any
---
generic object: Individual
question: What allergy does ${ x } have?
fields:
- Allergy: x.allergy[i]
---
generic object: Individual
question: Does ${ x } have any other allergies?
yesno: x.allergy.there_is_another
Docassemble sets x to the appropriate instance (e.g., person[0] or person[1].child[2]) before running the block. This avoids writing separate questions for person[i].allergy and person[i].child[j].allergy.
Gathering a DADict
Similar to DAList, but the gathering process also asks for a key via .new_item_name. In the value question, i is the key (not an integer):
objects:
- fruit: DADict
---
question: What fruit?
fields:
- Name: fruit.new_item_name
---
question: How many seeds does ${ i } have?
fields:
- Seeds: fruit[i]
---
question: Are there more fruits?
yesno: fruit.there_is_another
To set key and value in one question, set both .new_item_name and .new_item_value together in a single question block.
Gathering a DASet
Like DADict but uses a single .new_item attribute:
question: What item to add?
fields:
- Item: colors.new_item
For loops in Mako templates
Iterate over a gathered list inside question text with % for / % endfor:
question: Summary
subquestion: |
% for item in fruit:
- ${ item }
% endfor
Do not indent the text lines with spaces — Markdown treats indented text as a code block. The % for / % endfor directive lines themselves may be indented for readability but it is not required.