Skip to content

Questionnaire Lifecycle

A questionnaire goes through several transformations between the moment a content editor creates it in Strapi and the moment an end user sees it in the browser. Understanding this lifecycle helps you debug issues and know which layer is responsible for what.

flowchart TD
    A["1. Content editor creates<br/>questionnaire in Strapi"] --> B["2. Strapi stores structured<br/>data (metadata, questions,<br/>rules, calculated fields)"]
    B --> C["3. Strapi API transforms<br/>data to canonical format"]
    C --> D["4. Sync script fetches<br/>via API, wraps strings<br/>in t() for i18n"]
    D --> E["5. Generated .ts file<br/>committed to repo"]
    E --> F["6. App imports file,<br/>passes TFunction +<br/>interpolation data"]
    F --> G["7. qnr-framework renders<br/>questions one at a time"]
    G --> H["8. User answers →<br/>rules evaluated →<br/>next question shown"]

In the Strapi admin, a content editor creates a new Questionnaire entry with four main sections:

Section What it contains
Metadata Vertical link, question list (ordering), progress bar config, constants (reusable values), feature flags
Questions (dynamic zone) Each question as a component — with its type, questionId, groupId, props, screen config, and required flag
Rules (dynamic zone) Conditional navigation logic — “if X, go to Y”
Calculated fields Derived values computed from answers (used in rules or display)

Every question in the dynamic zone has these key properties:

Property Purpose Example
questionId Unique identifier within the questionnaire dateOfBirth, coverageStartDate
type The kind of input or screen (30+ types available) RADIO, DATE, ADDRESS, QUOTE, BLOCKER
groupId Which phase of the flow it belongs to intro, preQuote, postQuote, signup
required Whether the user must answer to complete the flow true / false
screen Display configuration: title, subtitle, description Title: “When would you like coverage to start?”
props Type-specific configuration Radio options, date constraints, validation rules

The groupId determines when the question appears relative to the quote page:

flowchart LR
    A["<b>intro</b><br/>Welcome screens,<br/>initial context"] --> B["<b>preQuote</b><br/>Questions needed<br/>before pricing"]
    B --> C["<b>Quote Page</b><br/>Shows plans,<br/>pricing, addons"]
    C --> D["<b>postQuote</b><br/>Additional details<br/>after plan selection"]
    D --> E["<b>signup</b><br/>Checkout, payment,<br/>processing, success"]

When the sync script calls the Strapi API, it does not get the raw database format. The questionnaire.js service in Strapi applies several transformations to produce a “canonical” format:

  • Constants: The meta.constants array of {key, value} pairs becomes a flat object {key: value}
  • Progress bar: The array of {initialQuestionId, title} entries becomes a mapping structure the framework can use
  • Questions: Dynamic zone components are flattened — the Strapi __component field is replaced with a clean type field
  • Rules: Rule components are simplified — questionId becomes id, outcome becomes then

These transforms exist so the CMS can use a rich editing experience (dynamic zones, nested components) while the app receives a clean, flat structure.

The sync script fetches the transformed data and performs one crucial additional step: it wraps all translatable strings with t() function calls.

For example, a question title like "When were you born?" becomes:

t('dateOfBirth.title', 'When were you born?')

This enables the i18n (internationalization) system — the first argument is the translation key, the second is the fallback default. The output is a .ts file in apps/app/src/syncedData/.

The generated file exports a function that takes a TFunction (from the i18n library) and optional interpolationData. When the app calls this function, all the t() wrappers resolve to actual translated strings.

With the resolved questionnaire definition in hand, qnr-framework takes over:

  1. Find the next question: Looks through the question list for the first unanswered required question
  2. Render the screen: Shows the question’s screen (title, subtitle, description) along with the appropriate input component
  3. Wait for the answer: User interacts with the component (selects a radio option, enters a date, etc.)
  4. Handle the answer: The onAnswer callback stores the answer in state
  5. Evaluate rules: Checks if any rules for this question should redirect the flow
  6. Advance: Moves to the next question (either from rules or the default question list order)

After each answer, the rules engine checks all rules associated with the current question. If a rule’s condition matches, the flow jumps to a different question instead of following the default order. This is how Birdhouse creates branching flows — for example, showing a blocker screen if the user is under 18, or skipping questions that do not apply to their situation.

See Rule Evaluation for the full details on how rules work.

Symptom Likely cause Where to look
Question not showing Missing from questionList in metadata, or marked not required with no rule leading to it Strapi → Questionnaire → Metadata
Question shows at wrong time Wrong groupId or incorrect position in questionList Strapi → Question component
Flow skips a question A rule is redirecting past it Strapi → Rules, or the generated .ts file
Text not appearing or showing a key Translation key missing or t() wrapper issue Generated .ts file, translation files
Changes not visible in app Sync not run, or synced file not deployed Run sync script, check PR status