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
- A slide rule that fires on a paragraph consumes it — later slide rules don't see the same paragraph a second time.
- Text rules run per-run on each slide. A rule that matches a bold run applies its style; the next text rule can still match a different run.
- Because Text rules run *after* the split into slides, a Text rule can't change which paragraphs become which slides. Use a Slide rule for that.
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.
kind | Value type | Meaning |
|---|---|---|
is_blank | none | Paragraph is empty / whitespace only |
is_image | none | Paragraph is an inline image |
starts_with | text | Paragraph text begins with value |
starts_with_two_dashes | none | Paragraph starts with -- |
all_bold | none | Every meaningful run in the paragraph is bold |
all_italic | none | Every meaningful run in the paragraph is italic |
all_underline | none | Every meaningful run in the paragraph is underlined |
all_text_color | color (see text_color below) | Every meaningful run has this text color |
starts_with_bold_underline | none | Paragraph LEADS with a bold+underlined phrase |
underlined_after_heading_prefix | none | After stripping a #<digit>— prefix, the rest is underlined |
single_line_slide | none | Slide body is a single line (no \n breaks) |
is_scripture_slide | none | Slide has a Bible reference or Scripture group label |
slide_type_is | text | Slide's type equals value (e.g. point, scripture) |
text_matches | text (regex) | Paragraph text matches the regex |
is_footnote_marker | none | Bible-gateway footnote marker ([a], etc.) |
followed_by_colon | none | Next character after the paragraph is : |
matches_inline_ref_body | none | Matches Reference: body on one line |
splits_into_ref_and_body | none | Reference is at the start or end of chunk |
contains_bible_reference | none | Any Bible reference anywhere inside |
bible_reference | none | Paragraph *is* a Bible reference |
next_paragraph_bible_reference | none | The next non-blank paragraph is a bare Bible reference |
previous_paragraph_bible_reference | none | The previous non-blank paragraph is a bare Bible reference |
verse_label | none | Paragraph is a [Verse 3]-style label |
person_name | none | Text approximates a person's name |
all_caps | none | Paragraph 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.
kind | Value type | Meaning |
|---|---|---|
highlight | color (yellow, green, blue, pink, purple) | Run has this word-processor highlight |
text_color | color (red, blue, green, gold, orange, purple, white) | Run text color |
text_color_hex | hex (#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_highlight | none | Run 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 |
bold | none | Run is bold |
italic | none | Run is italic |
underline | none | Run is underlined |
strike | none | Run has strikethrough |
font_size | choice (larger, smaller, same) | Compare against the paragraph's average |
regex | text (regex) | Run text matches the regex |
line_position | choice (first, middle, last) | Where the run sits on its line |
split_run_side | choice (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_count | number | Run 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
action | Fields | Effect |
|---|---|---|
split_on | keep_boundary (bool) | This paragraph closes the current chunk |
flush_and_emit | — | Flush the current chunk and emit this paragraph as its own slide |
span_between | open, close, outside ("context" or "keep") | Text between markers is the slide |
paragraph_matching | strip_prefix, strip_suffix | The paragraph itself is a slide (strip the markers) |
Assembly — reshape an already-selected candidate
action | Fields | Effect | ||
|---|---|---|---|---|
drop_candidate | — | Discard the candidate (e.g. footnote markers) | ||
set_group_label | label | Tag the slide with a group label | ||
set_theme_group | group | Force the slide to render in this named theme group | ||
set_slide_type | type | Set slide["type"] (e.g. point, sermon_theme, numbered_point) so downstream style rules can match slide_type_is | ||
join_paragraphs_with | separator | Join multiple paragraphs into one slide with this separator | ||
mark_author_line | flags | Recognise an author byline in a quote | ||
split_run_on_regex | pattern (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 |
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:
| Value | Meaning |
|---|---|
inline_capture | Match a Ref: body shape and capture the ref side |
positional_split | Whichever end of the chunk looks like a reference |
whole_selection | The whole chunk is the reference |
paragraph_minus_dashes | Paragraph with leading -- stripped |
whole_paragraph | The whole matched paragraph |
match_to_eol | The reference-shaped run, to end of line |
body_source — where the slide's body text comes from:
| Value | Meaning |
|---|---|
rest_of_inside | Everything inside the marker after the ref |
body_slice | The non-reference slice of the chunk |
outside_before / outside_after | Text outside the marker, before / after |
outside_then_preceding_block | Walk backwards across paragraphs to find a body |
next_body / previous_body | The next / previous paragraph |
next_then_previous_body | Try next first, else previous |
remaining_paragraphs | All remaining paragraphs in the chunk |
text_before_match | Text 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.
| Value | Meaning |
|---|---|
strip_boundary_separators | Trim commas / dashes at the chunk boundary |
strip_leading_colon | Trim a leading : |
strip_leading_ws_open_quote | Trim leading whitespace + open quote |
strip_verse_number_prefix | Trim leading 12-style verse numbers |
strip_trailing_close_quote | Trim a trailing close quote |
flatten_to_one_segment | Collapse 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)
action | Fields | Effect |
|---|---|---|
route | element (theme element name) | Route the matching run into that theme element |
style | color, weight, size, text_case, tracking, font_family, alignment, vertical_alignment, bold, italic, underline | Restyle the matching run (see below) |
split | — | Break the run onto its own line |
hide | — | Drop the run from the rendered slide |
strip_regex | pattern (regex string) | Remove the first regex match from the run's text — modifiers stay, only the text shrinks |
Style-attribute notes:
size— point size against a 1920×1080 canvas. The preview scales it; export uses it verbatim.font_family— PostScript family name (free-text; the editor offers a datalist of common families).alignment—left/center/right/justified. Lands on every matching run's style hash.vertical_alignment—top/middle/bottom.bold/italic/underline— booleans (not conditions). Settrueto force the modifier on,falseto strip it. Useful for normalising whole-paragraph emphasis (e.g.italic: falseto strip block-quote italics that Word applies to the first paragraph of a cited scripture passage).
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:
- Flat (default from the editor): ``
json "then": { "action": "style", "color": "#F7F2E8", "font_family": "Source Sans 3" }`` - Nested: ``
json "then": { "style": { "on": true, "font_family": "Lora", "italic": true, "highlight": null } }``
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)
add_blank_slides— insert a blank slide between distinct content chunks. Skipped between the continuation pieces of a split passage.split_long_text— break a long body across multiple slides at sentence boundaries when possible; the same reference rides along on every piece.treat_quotes_as_scripture— peel a Christian author name (C.S. Lewis, D.A. Carson, and similar) off the trailing edge of a quote slide intoslide["reference"], so quote attribution styles the same as a Bible citation.split_lists_as_points— when a slide body contains multiple non-blank lines (either soft breaks or paragraph-broken lines that the selector joined into one chunk), split into one slide per line and mark eachslide_type: "point". Pair with aslide_type_is: "point"style rule to apply point styling automatically.
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.
- Above each slide the chip legend shows the current Theme › Group › Choice —
routed by rulemeans aset_theme_grouprule picked the group;auto (scripture|point|blank)means slide-type inference did. - Every theme element gets a dashed light-gray outline with its name; runs routed by a rule get the brighter cyan outline instead.
- The Source paragraphs pane under each slide shows exactly which input paragraphs pp7gen mapped to that slide (via provenance from the engine).
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
- Source of truth:
docs/design_rules.mdin the WorshipKit web repo. - Served publicly at:
/design-rules(HTML) and/design-rules.md(raw markdown). - Discovered by AI agents via:
/llms.txtat the site root. - Kept up to date by: the "update the design-rules docs" contributor rule in
CLAUDE.md.
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.