4 Building a Sunday Service Planner Inside Rock RMS Shared by Sam DeSocio, Good News Church 11 days ago 19.2 Operations Intermediate THIS WAS A LATE NIGHT CLAUDE CODE PROJECT - IF I CAN DO IT YOU CAN! OverviewThis recipe walks through a Sunday service planning tool built entirely inside Rock — no external database, no third-party scheduling app. It plans the order of service (what happens, how long each thing takes, who's doing it, which songs get sung), computes a live running clock so you know before Sunday if the service runs long, and publishes a plan that a separate read-only volunteer view can safely consume.Everything lives in two Rock Content Channels and rides on Rock's existing group/security model. There's no second system to keep in sync.Why You'd Want ThisIf your team is planning services in a shared spreadsheet or a group text — song picks in one place, who's running slides in another, timing worked out by guesswork on Sunday morning — this replaces all of it with one source of truth that's already wired into the people and groups you manage in Rock.The part it's actually good at is time: give it a service that starts at 10:30 and a list of elements with durations, and it tells you the plan ends at 11:42 — before Sunday, not during it.Architecture at a GlanceTwo Content ChannelsChannelIDPurposeServices24One item per planned Sunday. Order of service, timing, and assignments live on it as a JSON blob (ElementsJson), plus a Boolean IsArchived attribute for soft-delete.Songs25The song library the run sheet's song rail pulls from.Watch this: if you're following older internal docs, double check which number is which — it's an easy transposition to make and it'll break your queries silently.Volunteer groups — the run sheet assigns people by pulling live rosters from existing Rock groups rather than storing names as text:MusicSlides / Sound / Video (three separate tech groups)Host & ReadersDelivery mechanism — a Lava Application Content block (not a plain HTML block) hosted on an internal-only page. All reads are raw SQL against the content channel and group tables, rendered straight into page HTML. Page security is the only gate on that data, and endpoints are reachable independently of the page — Rock only checks ExecuteView on the application itself. Grant that to your worship/leaders role only, and don't assume the page's nav hides anything.Prerequisites (Do These First)1. Create the Content Channel Type and ChannelsAdmin Tools → Communications → Content Channel Types — create a type for planning data. Keep it separate from any existing type (e.g. don't reuse one shared with a news/highlights channel) — the type-level item attributes apply to every channel using that type, so mixing purposes gets messy fast.Admin Tools → Communications → Content Channels — create your Services and Songs channels against that type. Note the numeric IDs; you'll use them everywhere (Lava, endpoints, security).2. Add the key attributesOn the Services channel item type, add at minimum:ElementsJson (Memo/large text) — the entire run sheet for that service lives here as one JSON blob. This is a deliberate trade-off: writes are atomic and reads are cheap, but you lose the ability to query Rock directly for "every service that used song X" — you'd parse the JSON client-side or build a reporting pass separately.IsArchived (Boolean) — a soft-delete flag. Keep it separate from your published/draft status rather than folding cancellation into status; a cancelled-but-published service is still published, just hidden.TargetEndMin (Integer) — the target service length in minutes, used by the run sheet's over/under timer. Don't set a Default Value on this attribute in Rock's admin UI. If your create endpoint does a raw SQL INSERT rather than going through Rock's save pipeline, the Rock-side default never fires — the field will look fine on screen (because your Lava falls back to a default at render time) and then silently revert on reload. Handle the default in Lava ({{ TargetEndMin | Default:75 }}) instead of relying on the attribute's configured default.3. Confirm your volunteer groups exist and note their IDsYou'll reference these directly in your Lava/SQL, so record the group IDs for Music, Slides, Sound, Video, Host, and Reader (or however your teams are split) before you start building queries.4. Create the Lava Application pageAdmin Tools → CMS Configuration → Pages — new internal page, add a Lava Application block (not a plain HTML/Lava block — the Application block type is what lets you register multiple server-side endpoints under one page).Set page security so only your worship/leaders role has view access, and grant ExecuteView on the application block to the same role — remember, that's the actual gate, not the page nav.The ViewsBuild this as a single Application with a view query-string parameter switching between modes:?view=dashboard — upcoming Sundays as cards (date, status, service time, a one-line summary like "68 min · 9 elements", who's assigned, open roles), plus a searchable archive of past services. Searching by any element in the plan (not just the sermon title) makes "what did we sing the week I preached on Jonah?" a single query.?view=runsheet&ServiceId={id} — the working view. Ordered list of elements with a running clock computed from the schedule's start time plus every preceding element's length. Elements flagged pre (pre-service) count backwardsfrom the start time instead of forwards. A detail rail lets you rename, retime, reassign, and add notes per element; a searchable song rail on the side is drag-and-drop onto the plan.?view=matrix — several Sundays side by side as columns of draggable chips, so you can reuse or duplicate a week's plan onto future Sundays instead of rebuilding from scratch.A separate volunteer-facing viewOnce the planner works, build a second, read-only Lava Application for volunteers — don't just loosen security on the planner. The planner exposes write endpoints (create/update/publish/delete); a volunteer with page access to that application can reach those endpoints directly regardless of what buttons you show them, since endpoint security is separate from UI. A dedicated read-only application sidesteps the whole problem.The volunteer view should show only published, non-archived, upcoming services: a "who's serving" card per team, the order of service with a running clock, and a print-ready run sheet. Pull "who's serving" from Rock's actual group scheduler (Attendance / AttendanceOccurrence) rather than from names typed into the plan's notes field — the latter drifts out of sync with real scheduling almost immediately.EndpointsThe Application ended up with nine server-side endpoints. Each is a separate registered method on the same Lava Application — there's no separate controller or page per action.EndpointStatus writeNotescreate-service→ draftRaw INSERT against the Services channel. Because it bypasses Rock's save pipeline, any attribute default configured in Rock's admin UI (see TargetEndMin above) won't apply — cover it in Lava instead.update-service(no status change)Added later, specifically to change a service's date/time after creation. Don't lock date/time at creation — you will need to move a service (weather, calendar conflicts) more often than you'd expect.save-order→ startedPersists a reordered ElementsJson blob after drag-and-drop on the run sheet.save-song / delete-song(Songs channel)Manage the song library independently of any service. delete-song is a true hard delete — cascades the item's AttributeValue rows and is hard-filtered to the Songs channel so it can't reach anything else.publish-service→ publishedAlso stamps every song used in the plan with a LastPlayedDate of that day. Watch this: if you later move the service's date with update-service, those stamps go stale until you republish — nothing currently blocks that, it's just a known gap.unpublish-service→ back to started/draftThe reverse of publish. Easy to forget when you first design the status machine — build it from day one rather than bolting it on once someone asks "how do I un-publish this?"archive-servicesets IsArchived = trueSoft delete, reversible. Keeps ElementsJson intact — everything that was actually sung and who served stays queryable. Requires the IsArchived attribute to exist; without it the endpoint returns an explicit error rather than silently no-op'ing.delete-service(row removed)Hard delete. Refuses to run against anything with status published — force an unpublish or archive first. Mirrors delete-song's cascade-and-hard-filter pattern.Security modelAll nine endpoints share one gate: ExecuteView on the Application block. There's no per-endpoint permission layer — granting that one role assignment authorizes everything the app can do, including delete. Page security controls who can see the page; it does not control who can hit an endpoint directly, since the endpoints are reachable independent of the page UI. If your Lava Application tooling exposes a security mode setting per-endpoint (ours had one, mislabeled on the older endpoints), confirm it actually resolves to the application-level check rather than trusting the label.Leave CSRF protection at its default (on) and make sure your client-side request helper sends whatever CSRF header your Lava Application framework expects on every write call — it's easy to have this working on the endpoints you tested and silently broken on ones you added later and copy-pasted from an older, pre-CSRF version.Blast radius, if a credential or session were ever misused: every endpoint is hard-filtered to the two content channels above — no path to person, group, or financial data. Once delete-service exists, the worst realistic outcome is a deleted plan (recoverable if you keep the standard Rock backup cadence), not a deleted person record or a compromised giving history. Worth stating explicitly if you're getting this past a board or a more security-conscious tech lead.Gotchas We HitA few things cost real debugging time and are worth flagging so you don't repeat them:CSS comments containing HTML-like text can truncate Lava rendering entirely. A /* ... */ CSS comment that happened to contain a quoted HTML attribute (e.g. <a class="...">) caused the whole template to stop rendering mid-stream, with no error — just a blank page past that point. If a template that used to render suddenly goes blank after a style-only edit, check your CSS comments before assuming it's a size limit or something structural.Don't trust Lava's string filters (Truncate, Left, Replace, etc.) for anything you can compute in SQL instead.They produced inconsistent output during this build. Doing name/initial formatting in the SQL query itself was more reliable than doing it in Lava.Printing from inside a Rock page needs explicit isolation. Rock's page chrome will print above your content unless you lift the print target out to document.body. You'll also want print-color-adjust: exact if you're relying on background colors for a printed run sheet.Bare <button> elements inside Rock's ASP.NET form wrapper default to type="submit". If a button is meant to be a client-side action (drag handle, toggle, etc.) and you forget to mark it type="button", clicking it triggers a full page postback instead.Design Decisions & Trade-offsElementsJson as a blob, not rows — cheap reads, atomic writes, unqueryable from Rock directly.IsArchived as a flag, not a status — avoids having to guess the "before" status when un-archiving.Raw SQL reads, gated by page + endpoint security, not Rock's entity security — fine for an internal tool, but it means this belongs on an internal-only page, never anything public-facing.Still to ComeVolunteer notifications — publishing a plan doesn't yet text/email the assigned volunteers; that needs a workflow type wired to publish-service.Reporting — nothing currently reads ElementsJson outside the app itself. Song frequency and service-length trends over time are computable from what's already stored, just not built yet.Questions or improvements? Reply below or reach out — happy to share more detail on any of the pieces above. Download File