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_emitFlush 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_candidateDiscard 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
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

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)
splitBreak the run onto its own line
hideDrop 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)


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.