Mental model
A proposal is a suite of sections. Each visual block of the document (intro, goals, pricing, testimonials…) is wrapped in asection. Anything
outside a section uses page-level styling.
Structural rules (enforced by Zod + a runtime validator with explicit
error messages):
- The root of any tree submitted via the DSL is a
sectionorsection[]. - A
sectioncannot contain anothersection— flat hierarchy at the root. - Inside a section, only blocks are allowed: paragraph, heading, columns, cards, mini_table, gallery, etc.
columns.colsmust equalcolumns.length.image/icon/videoare placeholders — nosrcaccepted.embedis the only block that takes an external URL (with provider-host allowlist).- Card-grouped components (
testimonial_cards,process_cards, …) always assemble to acolumnsblock with one card per column. Proprietary card containers are never produced.
Schema discovery
Agents use two tools to learn the grammar at runtime:list_propal_components
Returns the full catalog with descriptions, structural rules, limits, and
a compact example per component.
get_propal_component_schema
Returns the detailed Zod-derived schema for a single component
(
{ component_type: 'section' | 'columns' | ... }).Layout
Basic blocks
Media
Placeholders only — no external URLs:
The user uploads the actual asset in the editor after the agent has built the
structure.
With URL:
Business / rich blocks
Cards
For each card type the DSL exposes both a standalone form and a grouped form. Grouped cards always assemble to acolumns block with one
card per column — proprietary containers (testimonialsContainer,
accordionContainer, etc.) are deprecated and never produced.
Grouped cards take a
cols prop matching the visual layout ('1'–'4').
Section theming
Three accepted forms forsection.section_style, in order of preference:
'default'— “Page” : the section inherits page-level colors. No section ambience.- Named slot id (recommended) — e.g.
'soft','deep'. Resolved server-side viatheme.section_styles[].id. Survives reordering of the theme’s slots. - Positional index —
'1','2','3', … 1-indexed reference into the theme’ssection_styles[]array. In stock themes:'1'= Default,'2'= Highlight,'3'= Contrast.
section.theme (white / gray / black / outlined) is
inert when a custom theme is applied to the proposal — it only controls
the legacy bodyTheme fallback on themeless proposals. Prefer section_style.
See theming for the full picture.
Background image
media_id must reference an asset that already exists in the org’s media
library. The server resolves the URL at assemble time.
Limits
These are exposed in the
limits field of list_propal_components.
End-to-end example
Build a 2-section proposal with a hero and three numbered callouts:"highlight" if a
matching slot id exists), assembles the TipTap nodes, and atomically appends
them with optimistic concurrency control.
Round-trip stability
Reading a proposal viaget_proposal_tree returns the same DSL shape you
wrote — assembler defaults are stripped on read so the round-trip is a
fixed point. Caveats:
- Node types not in the DSL come back as
{ type: 'unknown', tiptap_type, text_preview, child_count }. - Old proposals containing legacy
*Containernodes are flattened to the grouped DSL form (testimonial_cards,questions, …) on read only — they are never written back in that form. - Headings with
level > 3come back asunknown(the DSL only supports 1-3).