WorshipKit Design Rules Guide — also available as raw markdown. AI agents can discover this via /llms.txt.

WorshipKit Design Rules

A guide to the rule-based Design editor at /slides/designs/:id — for church admins building their own designs, for their AI assistants helping them, and for the WorshipKit team.

Public URL: https://worshipkit.com/design-rules Also served as raw Markdown at: https://worshipkit.com/design-rules.md

If you're an AI agent helping a WorshipKit user set up a Design, read this whole page first — every wire-format detail below matches what the engine actually consumes.


What a Design is

A Design is a saved recipe that turns a document (Word, Pages, or PowerPoint) into ProPresenter slides. It has three pieces:

1. Slide rules (segment_rules) — decide which paragraphs become slides, how they're chunked, and how references are extracted. 2. Text rules (style_rules) — decide how the runs on those slides are styled and routed to theme elements. 3. Options — post-process switches: add_blank_slides, split_long_text, treat_quotes_as_scripture, split_lists_as_points.

The rules run top-down through two phases. Slide rules run first (the Selector / Assembler in pp7gen), then post-process options apply, then Text rules run (the StylePass). First match wins within each rule's condition.


The pipeline (why order matters)


input paragraphs
      ↓
[ Slide rules ]  ← segment_rules, top-down, first match wins
      ↓
[ apply_slide_options ]  ← split_lists_as_points + split_long_text + add_blank_slides
      ↓
[ Text rules ]   ← style_rules, top-down, first match wins per run
      ↓
rendered slides

Rule anatomy

Every rule is a JSON object with the same top-level shape:


{
  "id": "r-my-rule",
  "name": "Human-readable name",
  "enabled": true,
  "when": { /* a Condition (leaf or group) */ },
  "then": { /* an Action */ }
}

Conditions

A condition is a leaf (single check) or a group (AND / OR of children).

Leaf:


{ "t": "c", "kind": "bold", "negate": false }

Group (AND / OR of children):


{
  "t": "g",
  "op": "AND",
  "children": [
    { "t": "c", "kind": "bold", "negate": false },
    { "t": "c", "kind": "highlight", "value": "yellow" }
  ]
}

negate: true inverts a leaf. Groups can nest.

Condition kinds (kind: values)

Paragraph-scoped — evaluated once per paragraph.

kindValue typeMeaning
is_blanknoneParagraph is empty / whitespace only
is_imagenoneParagraph is an inline image
starts_withtextParagraph text begins with value
starts_with_two_dashesnoneParagraph starts with --
all_boldnoneEvery meaningful run in the paragraph is bold
all_italicnoneEvery meaningful run in the paragraph is italic
all_underlinenoneEvery meaningful run in the paragraph is underlined
all_text_colorcolor (see text_color below)Every meaningful run has this text color
starts_with_bold_underlinenoneParagraph LEADS with a bold+underlined phrase
underlined_after_heading_prefixnoneAfter stripping a #<digit>— prefix, the rest is underlined
single_line_slidenoneSlide body is a single line (no \n breaks)
is_scripture_slidenoneSlide has a Bible reference or Scripture group label
slide_type_istextSlide's type equals value (e.g. point, scripture)
text_matchestext (regex)Paragraph text matches the regex
is_footnote_markernoneBible-gateway footnote marker ([a], etc.)
followed_by_colonnoneNext character after the paragraph is :
matches_inline_ref_bodynoneMatches Reference: body on one line
splits_into_ref_and_bodynoneReference is at the start or end of chunk
contains_bible_referencenoneAny Bible reference anywhere inside
bible_referencenoneParagraph *is* a Bible reference
next_paragraph_bible_referencenoneThe next non-blank paragraph is a bare Bible reference
previous_paragraph_bible_referencenoneThe previous non-blank paragraph is a bare Bible reference
verse_labelnoneParagraph is a [Verse 3]-style label
person_namenoneText approximates a person's name
all_capsnoneParagraph text is ALL CAPS

"Meaningful runs" — all_bold / all_italic / all_underline ignore runs that are pure whitespace or punctuation, and treat a word-processor highlight as equivalent emphasis. That way a Word document that splits Advantage of a Helper into [Advantage][ ][of][ ][a][ ][Helper] runs still matches all_underline even though the space runs aren't underlined.

Neighbour Bible-reference guards. Use next_paragraph_bible_reference (with negate: true) on a paragraph selector when the next paragraph is a bare reference that will pull this paragraph's text in as its scripture body (via body_source: previous_body — see the extractor table below). Without the guard, the same paragraph emits twice: once as its own slide, and again as the body of the following scripture slide. previous_paragraph_bible_reference is the symmetric guard for the reverse layout.

Run-scoped — evaluated per run. A paragraph matches if *any* of its runs match, unless the rule engine says otherwise.

kindValue typeMeaning
highlightcolor (yellow, green, blue, pink, purple)Run has this word-processor highlight
text_colorcolor (red, blue, green, gold, orange, purple, white)Run text color
text_color_hexhex (#00a2ff)Run's raw text-color hex (case-insensitive) — use when the semantic palette name is too coarse (e.g. two source shades both snap to blue)
comment_highlightnoneRun carries a word-processor comment (Pages / Word inline comments) — surfaced by pages_input_strategy as an emphasis signal when no other run-level style discriminator is available
boldnoneRun is bold
italicnoneRun is italic
underlinenoneRun is underlined
strikenoneRun has strikethrough
font_sizechoice (larger, smaller, same)Compare against the paragraph's average
regextext (regex)Run text matches the regex
line_positionchoice (first, middle, last)Where the run sits on its line
split_run_sidechoice (first, last)The run was produced by a preceding split_run_on_regex action, and this is the side (LHS / RHS) it came from. Negated with no value ({negate: true}) matches runs that were NOT produced by a split — useful for a fallback style rule alongside per-side rules
word_countnumberRun word count ≥ N
wrapped{ open, close }Text is wrapped in these markers
neighbor_wrapped{ direction, open, close }The paragraph before / after is wrapped

Color conditions store a stable *name* (not the hex) so a theme swap doesn't invalidate the rule.

Palette scope. The text_color palette is deliberately narrow. Explicit black is not included — most word processors treat black as the default body text, so a text_color: "black" condition would fire on every plain paragraph. White *is* included: authors only set white text when they've paired it with a dark paragraph fill (a distinctive block layout — sermon-theme cards, callouts), so it's a high-signal match. Everything else in the grey axis (mid-grey, off-white below the near-white cutoff) is treated as "no explicit color."


Slide rule actions (segment_rules[].then)

Selectors — decide what becomes a slide

actionFieldsEffect
split_onkeep_boundary (bool)This paragraph closes the current chunk
flush_and_emit—Flush the current chunk and emit this paragraph as its own slide
span_betweenopen, close, outside ("context" or "keep")Text between markers is the slide
paragraph_matchingstrip_prefix, strip_suffixThe paragraph itself is a slide (strip the markers)

Assembly — reshape an already-selected candidate

actionFieldsEffect
drop_candidate—Discard the candidate (e.g. footnote markers)
set_group_labellabelTag the slide with a group label
set_theme_groupgroupForce the slide to render in this named theme group
set_slide_typetypeSet slide["type"] (e.g. point, sermon_theme, numbered_point) so downstream style rules can match slide_type_is
join_paragraphs_withseparatorJoin multiple paragraphs into one slide with this separator
mark_author_lineflagsRecognise an author byline in a quote
add_actionsactions (array — see below)Attach one or more ProPresenter cue-level actions (audience look, timer, macro) to the emitted slide. Fires ONCE per passage even when split_long_text fans a scripture chunk across multiple cues
split_run_on_regexpattern (regex), keep_boundary (drop \left \right)Split each matching segment into two runs at the first regex match, tagging the LHS/RHS with split_run_side for downstream style_rules to key on. keep_boundary controls whether the matched separator itself is dropped, kept on the left run, or kept on the right run

Cue-level actions (then.actions)

Any segment rule with an assembly action may also carry an actions: array — either on the standalone add_actions shape or piggybacked on set_slide_type, set_group_label, set_theme_group, etc. Each entry attaches one ProPresenter action to the generated slide's cue.


{
  "when": { "t": "c", "kind": "slide_type_is", "value": "scripture" },
  "then": {
    "action": "add_actions",
    "actions": [
      { "kind": "audience_look", "name": "Scripture" }
    ]
  }
}

Combined with a slide-type rule in a single pass:


{
  "when": { "t": "c", "kind": "splits_into_ref_and_body" },
  "then": {
    "reference_source": "positional_split",
    "body_source": "body_slice",
    "group_label": "Scripture",
    "actions": [ { "kind": "audience_look", "name": "Scripture" } ]
  }
}

Two rules attaching the same (kind, name) de-duplicate — the cue gets one action, not two. Style rules deliberately do NOT support then.actions; actions belong at the cue level, not per-run.

kindFieldsProPresenter actionStatus
audience_looknameAction::AudienceLookTypeEmitted
timername, timer_action (start \stop \reset \reset_and_start \stop_and_reset), duration_secondsAction::TimerTypeEmitted
macronameAction::MacroTypeEmitted
propname, prop_action (trigger \clear)Action::PropTypeEmitted

By-name resolution. Audience-look / timer / macro name values are looked up against the church's ProPresenter workspace at export time. When ProPresenter can't find a match, pp7gen logs a warning and skips the action — no made-up UUIDs, no crashed export.

Workspace-sourced pickers in the editor. The DesignEditor's action row renders a <select> populated from the org's imported ProPresenter workspace (GET /api/v2/slides/workspace/audience_looks, .../timers, .../macros) — no freeform typing. Picking an option stores both name (what pp7gen resolves against today) and uuid (stable identity for a workspace rename) on the entry. If the workspace hasn't been imported yet, the picker disables and surfaces an "Import your ProPresenter workspace" hint so the designer knows where to go.

set_slide_type values are free-text but by convention the generator + themes recognise point, sermon_theme, scripture, numbered_point, song, blank, and any lower-snake-case identifier a design defines. Pair with a slide_type_is style rule to apply per-type styling.

Reference extraction — pull a citation out of the chunk

Reference extraction rules have no action key — the presence of reference_source is what tells the engine to dispatch to the ReferenceExtractor.


"then": {
  "reference_source": "positional_split",
  "body_source":       "body_slice",
  "cleanups":          ["strip_boundary_separators"],
  "group_label":       "Scripture"
}

reference_source — where the reference text comes from:

ValueMeaning
inline_captureMatch a Ref: body shape and capture the ref side
positional_splitWhichever end of the chunk looks like a reference
whole_selectionThe whole chunk is the reference
paragraph_minus_dashesParagraph with leading -- stripped
whole_paragraphThe whole matched paragraph
match_to_eolThe reference-shaped run, to end of line

body_source — where the slide's body text comes from:

ValueMeaning
rest_of_insideEverything inside the marker after the ref
body_sliceThe non-reference slice of the chunk
outside_before / outside_afterText outside the marker, before / after
outside_then_preceding_blockWalk backwards across paragraphs to find a body
next_body / previous_bodyThe next / previous paragraph
next_then_previous_bodyTry next first, else previous
remaining_paragraphsAll remaining paragraphs in the chunk
text_before_matchText preceding the matched reference

cleanups — post-processing on the extracted text. String forms run against reference-body extractions; hash forms ({"kind": "...", ...args}) also run for paragraph_matching rules with no reference_source, so a selector rule can trim segments before the slide is emitted.

ValueMeaning
strip_boundary_separatorsTrim commas / dashes at the chunk boundary
strip_leading_colonTrim a leading :
strip_leading_ws_open_quoteTrim leading whitespace + open quote
strip_verse_number_prefixTrim leading 12-style verse numbers
strip_trailing_close_quoteTrim a trailing close quote
flatten_to_one_segmentCollapse runs into a single segment
{ "kind": "trim_segments_not_matching_color", "color"?, "color_hex"? }Drop every segment whose text_color (or text_color_hex) doesn't match the target. Empty target → no-op.

group_label (optional) — same effect as set_group_label; a quick way to say "this is a Scripture slide."


Text rule actions (style_rules[].then)

actionFieldsEffect
routeelement (theme element name)Route the matching run into that theme element
stylecolor, weight, size, text_case, tracking, font_family, alignment, vertical_alignment, bold, italic, underlineRestyle the matching run (see below)
split—Break the run onto its own line
hide—Drop the run from the rendered slide
strip_regexpattern (regex string)Remove the first regex match from the run's text — modifiers stay, only the text shrinks

Style-attribute notes:

Element-intent promotion. When a matching run is the *first* segment on the slide AND its style hash declares alignment or vertical_alignment, the generator promotes alignment, vertical_alignment, font_size, and font_family to the whole slide element (the theme's text box). This is the reliable signal that the rule intends whole-element styling (a slide-type body rule like "Point slides = Source Sans 3 centered, 114pt") rather than a per-run emphasis. A rule that only sets color / weight / font_family leaves the element bounds + font alone, so per-run emphasis (like a Black Diamond Emphasis accent on one word inside a scripture body) doesn't accidentally reshape the whole text box.

Nested then.style shape

Two wire shapes are accepted:

The nested shape is required when you want to set highlight: null (clear a word-processor highlight so a downstream emphasis carries the accent instead of the yellow rectangle). The flat shape's serializer drops nil values, so highlight: null survives only in the nested form.


Options (post-process)


Preceding / trailing boilerplate slides

Every design has two optional "boilerplate" slots — decks the user authors once and gets attached to every conversion that uses that design:

Each slot has:

Boilerplate slides are ordinary ProPresenter slides — every action, element, background image, transition, and cue-note the user set on them is preserved verbatim in the merged output. No blank slide is inserted between the boilerplate and the converter's cues; add one to the boilerplate deck explicitly if you want a spacer.

The join happens on every import path that resolves a design — Quick Import, Draft Review + Generate, AI Import, and legacy /slides/converter. Presentation import (third-party .pptx / .key / .pro / .proBundle) does not attach boilerplate today.


Common recipes

Scripture with the reference at the end


{
  "when": { "t": "c", "kind": "splits_into_ref_and_body" },
  "then": {
    "reference_source": "positional_split",
    "body_source": "body_slice",
    "group_label": "Scripture"
  }
}

Sermon points wrapped in [brackets]


{
  "when": { "t": "g", "op": "AND", "children": [] },
  "then": { "action": "span_between", "open": "[", "close": "]", "outside": "context" }
}

Route yellow-highlighted runs into the Emphasis element


{
  "when": { "t": "c", "kind": "highlight", "value": "yellow" },
  "then": { "action": "route", "element": "Emphasis" }
}

Hide ALL-CAPS section headers


{
  "when": { "t": "c", "kind": "all_caps" },
  "then": { "action": "hide" }
}

Bold + underlined runs become green Courier at 110pt


{
  "when": {
    "t": "g", "op": "AND",
    "children": [
      { "t": "c", "kind": "bold" },
      { "t": "c", "kind": "underline" }
    ]
  },
  "then": {
    "action": "style",
    "color": "#22c55e",
    "weight": "bold",
    "size": 110,
    "font_family": "Courier New"
  }
}

Point slides render in 114pt Source Sans 3 centered on parchment


{
  "when": { "t": "c", "kind": "slide_type_is", "value": "point" },
  "then": {
    "action": "style",
    "color": "#F7F2E8",
    "font_family": "Source Sans 3",
    "weight": "bold",
    "size": 114,
    "alignment": "center",
    "vertical_alignment": "middle"
  }
}

The alignment + vertical_alignment are the intent signal that promotes font_family + size to the whole slide element.

Whole-paragraph bold+underline → typed as a Point slide


{
  "when": {
    "t": "g", "op": "AND",
    "children": [
      { "t": "c", "kind": "all_bold" },
      { "t": "c", "kind": "all_underline" }
    ]
  },
  "then": { "action": "set_slide_type", "type": "point" }
}

Single-line slides starting with #1— → strip the marker + type as a numbered_point

Two rules — one segment rule to type, one style rule to strip:


{
  "when": {
    "t": "g", "op": "AND",
    "children": [
      { "t": "c", "kind": "single_line_slide" },
      { "t": "c", "kind": "regex", "value": "^#\\d+[-‐‑‒–—―]" },
      { "t": "c", "kind": "is_scripture_slide", "negate": true }
    ]
  },
  "then": { "action": "set_slide_type", "type": "numbered_point" }
}

{
  "when": {
    "t": "g", "op": "AND",
    "children": [
      { "t": "c", "kind": "single_line_slide" },
      { "t": "c", "kind": "line_position", "value": "first" },
      { "t": "c", "kind": "regex", "value": "^#\\d+[-‐‑‒–—―]" }
    ]
  },
  "then": { "action": "strip_regex", "pattern": "^#\\d+[-‐‑‒–—―]\\s*" }
}

Highlighted words on point / sermon_theme slides → Lora italic, clear the highlight


{
  "when": {
    "t": "g", "op": "AND",
    "children": [
      { "t": "c", "kind": "highlight" },
      { "t": "g", "op": "OR", "children": [
        { "t": "c", "kind": "slide_type_is", "value": "point" },
        { "t": "c", "kind": "slide_type_is", "value": "sermon_theme" }
      ] }
    ]
  },
  "then": { "style": { "on": true, "font_family": "Lora", "italic": true, "highlight": null } }
}

Iterating in the editor

The Live preview pane on the right runs your rules against either a uploaded document (.docx / .pages / .pptx) or a two-slide sample deck (a random C.S. Lewis quote + John 3:16-19 NKJV) when no upload is present.


For AI agents

If a WorshipKit user asks you to help set up a design, the reliable workflow is:

1. Ask what the source document looks like (Bible references at end of paragraph? Bracketed callouts? Asterisk-marked points?). 2. Pick a starting stock design (Default, Brackets, Asterisks) or Custom slide rule from the + New rule menu. 3. Compose conditions using only the kind values above — no other names are recognised. 4. Recommend add_blank_slides: true and split_long_text: true for sermons; both are safe defaults. 5. When guessing hex codes for style.color, prefer stable Tailwind swatches (e.g. #facc15 for gold, #96d35f for a highlight green) so a theme swap still reads sensibly.


Where this lives

If you add a new condition kind, action, cleanup, or option to either the pp7gen engine or the frontend editor, update this file in the same commit.