If you’ve ever tried to ship a complex HubSpot module — a quiz, calculator, or configurator — you’ve probably asked the same question: why does everything start empty, even when defaults are defined? This article explains why that happens, what HubSpot actually allows, and which patterns work in production.
fields.json Expectations vs Reality
At first glance, HubSpot modules look declarative. You define structure and defaults in fields.json, and it feels natural to expect the module to arrive with meaningful demo content.
In theory, this would mean:
- the module is added to a page;
- repeaters are already populated;
- demo content is ready to use;
- the editor tweaks instead of building from scratch.
Unfortunately, this expectation does not match how HubSpot modules actually work.
Defaults Are Not Seed Data
The most important distinction to understand is this:
Defaults in fields.json are fallback values, not content initialization.
However, in practice, HubSpot’s editor does not apply defaults eagerly. Some default values — especially inside repeaters — may only appear after the editor re-normalizes data in response to user interaction. This behavior is undocumented and inconsistent, and should not be relied on.
They exist to:
- define field structure;
- provide UI hints;
- prevent null rendering.
They do not:
- create repeater items;
- populate nested fields;
- act as demo content.
This difference is subtle, poorly documented, and responsible for most confusion.
Example: Default on a Simple Field
{
"name": "quiz_title",
"label": "Quiz Title",
"type": "text",
"default": "What Is Your Leadership Style in a Crisis?"
}
This may appear prefilled in some cases — but this behavior does not extend to repeaters.
Where It Breaks First: Repeaters
Simple fields behave as expected. A text field with a default value will render correctly. Repeaters behave differently.
If a field group is marked as repeatable, HubSpot will never auto-create items for it — even if every child field has a default value.
The result is always the same:
- the repeater exists;
- item count is zero;
- the editor UI is empty.
This is intentional and consistent behavior.
Example: Repeatable Group With Defaults That Will Not Appear
{
"name": "questions",
"label": "Questions",
"type": "group",
"occurrence": {
"min": 0,
"max": null
},
"children": [
{
"name": "question_text",
"type": "text",
"default": "When everything breaks, what do you do first?"
}
]
}
Despite the default value, no question item will be created.
Nested Repeaters: No Escape Hatch
Most real-world modules are not flat. In quizzes and assessments, repeaters are often nested multiple levels deep.
For example:
- Questions
- Answers
- Scoring rules
HubSpot provides no supported mechanism to prefill this hierarchy:
- not via Design Manager;
- not via the browser editor;
- not via CLI;
- not via undocumented flags.
Once this limitation is understood, many confusing behaviors suddenly make sense.
Example: Nested Repeatable Structure
{
"name": "questions",
"type": "group",
"occurrence": { "min": 0 },
"children": [
{
"name": "question",
"type": "text"
},
{
"name": "answers",
"type": "group",
"occurrence": { "min": 0 },
"children": [
{
"name": "answer_text",
"type": "text"
},
{
"name": "trait",
"type": "text"
},
{
"name": "delta",
"type": "number"
}
]
}
]
}
No part of this structure will be auto-populated.
The CLI Preset Illusion: A Real-World Case
While working on a production quiz module with deeply nested repeaters (questions → answers → scoring rules), I attempted to prepare a full demo preset via the HubSpot CLI by defining complete default values in fields.json.
When the module was added to a page, the result looked broken:
- some helper texts were missing;
- boolean toggles appeared disabled;
- repeater items shared duplicated values;
- nested answer data appeared empty.
At first glance, this looked like partial failure of defaults — or even a CLI bug.
However, something unexpected happened.
After adding or removing any repeater item in the editor UI, all previously broken fields suddenly normalized:
- missing defaults appeared;
- duplicated values corrected themselves;
- nested repeaters populated correctly.
This behavior creates the illusion that repeaters can be fully prefilled via CLI — but that conclusion is misleading.
What Actually Happened: Lazy Editor Normalization
HubSpot does not eagerly apply defaults when a module is first added to a page. Instead, the editor performs partial initialization and defers full normalization until the user mutates repeater data.
When a repeater item is added or removed, the editor re-processes the entire structure:
- missing values are filled from defaults;
- undefined fields are normalized;
- nested structures are reconciled.
This process is internal, undocumented, and not guaranteed to run on initial render.
The CLI did not “seed” content. It merely defined a schema that the editor later reconciled during user interaction.
Can This Be Done in the Browser?
No.
The Design Manager UI allows you to define module structure, but it does not provide any way to inject instance data when a module is added to a page.
Every module instance starts empty by design.
What About the HubSpot CLI?
This is a common assumption and an understandable one.
The HubSpot CLI operates on module source files, not on page instances. Fetching or pushing a module only updates its definition.
It does not:
- prefill editor data;
- modify existing page instances;
- seed repeater content.
Example: CLI Commands That Do Not Seed Content
hs fetch /my-module
hs upload /my-module
These commands sync schema — nothing more.
In some cases, deeply defined defaults may appear to populate after editor interaction. This is a side effect of internal editor normalization — not a supported content seeding mechanism.
Why HubSpot Works This Way
This limitation is not a bug — it is a deliberate architectural decision.
HubSpot modules are designed to be:
- safe for non-technical editors;
- predictable across updates;
- free from silent content mutations.
Allowing modules to auto-seed complex data would introduce serious risks:
- content being overwritten on updates;
- unexpected editor-side mutations;
- unstable Marketplace behavior.
HubSpot chose content ownership over automation.
The Demo Content Problem
This design choice creates a real challenge for complex modules:
- quizzes;
- calculators;
- multi-step assessments;
- interactive flows.
Without demo content, first-time UX suffers and support questions increase.
Why This Should Not Be Used as a Preset Strategy
Relying on editor-side normalization for demo content is risky:
- users may save the page before normalization occurs;
- behavior varies between modules and updates;
- Marketplace reviewers do not treat this as supported functionality.
If demo content matters, it must be controlled at runtime — not inferred from editor side effects.
Patterns That Actually Work
Runtime Seeding via HUBL (Server-First)
The most reliable approach is to resolve demo versus real data at the template level, before JavaScript execution.
Instead of trying to prefill or mutate iterable fields at runtime, the module decides which dataset to expose based on a clear condition.
This allows:
- fully nested and predictable data structures;
- no mutation of editor-configured fields;
- clear separation between authoring logic (HUBL) and runtime behavior (JavaScript).
This is the approach used in the Persona Scoring Quiz module.
Example: Runtime Seeding Pattern
{% set raw_questions = module.questions %}
{% set raw_profiles = module.profiles %}
{# Fallback decision is resolved server-side #}
{% set use_demo_data =
raw_questions|length == 0
or raw_profiles|length == 0
%}
{% if use_demo_data %}
{# Hardcoded demo data (safe, complete, predictable) #}
{% set final_questions = [
{
"question": "Demo question?",
"answers": [
{ "label": "Yes", "score": 1 },
{ "label": "No", "score": 0 }
]
}
] %}
{% set final_profiles = [
{ "key": "demo", "label": "Demo Result" }
] %}
{% else %}
{# Real editor data #}
{% set final_questions = raw_questions %}
{% set final_profiles = raw_profiles %}
{% endif %}
Explicit Demo Mode Toggle via HUBL
For complex modules, heuristic checks are often not enough. A more robust solution is to expose an explicit Demo Mode toggle.
This makes behavior deterministic and fully controlled by the editor:
- demo content on;
- editor content ignored.
Example: Demo Mode Toggle via HUBL
{% set force_demo = module.advanced.force_demo_data %}
{% set raw_questions = module.questions %}
{% set raw_profiles = module.profiles %}
{% if force_demo %}
{# Explicit demo mode — no guessing #}
{% set final_questions = [
{
"question": "Demo question?",
"answers": [
{ "label": "Option A", "score": 1 },
{ "label": "Option B", "score": 2 }
]
}
] %}
{% set final_profiles = [
{ "key": "demo_a", "label": "Demo Profile A" },
{ "key": "demo_b", "label": "Demo Profile B" }
] %}
{% else %}
{# Normal operation #}
{% set final_questions = raw_questions %}
{% set final_profiles = raw_profiles %}
{% endif %}
const quizContainer = document.querySelector('[data-quiz-container]');
const isDemoMode = quizContainer?.dataset.forceDemo === 'true';
// JS does NOT decide what data to use.
// It only adapts behavior.
if (isDemoMode) {
logger.warn('Demo Mode is enabled. Editor data is ignored.');
}
// Safe to assume data integrity at this point
initQuiz({
questions: window.QUIZ_QUESTIONS,
profiles: window.QUIZ_PROFILES
});
What Will Never Work
- defaults in repeaters;
- nested default propagation;
- CLI-based content injection;
- hidden or undocumented flags.
If someone claims otherwise, they are confusing schema with instance data.
Final Takeaway
If you are building advanced HubSpot modules:
- treat
fields.jsonas schema only; - move demo logic to runtime;
- document the behavior clearly;
- respect content ownership;
- editor normalization effects exist, but must not be treated as API or preset functionality.
HubSpot modules are powerful — once their boundaries are understood.