Skip to main content
The Propal components DSL is a typed JSON grammar that produces valid TipTap editor content without exposing the raw editor JSON. Agents author sections by composing typed blocks; the server validates against Zod schemas, applies structural rules, and assembles the final document atomically.

Mental model

A proposal is a suite of sections. Each visual block of the document (intro, goals, pricing, testimonials…) is wrapped in a section. 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 section or section[].
  • A section cannot contain another section — flat hierarchy at the root.
  • Inside a section, only blocks are allowed: paragraph, heading, columns, cards, mini_table, gallery, etc.
  • columns.cols must equal columns.length.
  • image / icon / video are placeholders — no src accepted.
  • embed is the only block that takes an external URL (with provider-host allowlist).
  • Card-grouped components (testimonial_cards, process_cards, …) always assemble to a columns block 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' | ... }).
The descriptions in those tools are the source of truth — this page mirrors them but the live tool always wins.

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:
Allowed hosts:

Business / rich blocks

Cards

For each card type the DSL exposes both a standalone form and a grouped form. Grouped cards always assemble to a columns 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 for section.section_style, in order of preference:
  1. 'default' — “Page” : the section inherits page-level colors. No section ambience.
  2. Named slot id (recommended) — e.g. 'soft', 'deep'. Resolved server-side via theme.section_styles[].id. Survives reordering of the theme’s slots.
  3. Positional index'1', '2', '3', … 1-indexed reference into the theme’s section_styles[] array. In stock themes: '1' = Default, '2' = Highlight, '3' = Contrast.
The legacy 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:
Send via:
The server validates the tree, resolves named refs (here "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 via get_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 *Container nodes are flattened to the grouped DSL form (testimonial_cards, questions, …) on read only — they are never written back in that form.
  • Headings with level > 3 come back as unknown (the DSL only supports 1-3).