Ministry Goals is a goal-tracking app for iPhone, iPad, and Mac designed to help you set, measure, and stay accountable to the goals that matter most. It supports a variety of metric types — streaks, cumulative counts, cadence targets, score averages, and more — so you can track progress the way that fits your work. Download Ministry Goals from the App Store:

Ministry Goals integrates with TRMNL, an e-ink display device that gives you a calm, always-visible dashboard of your goal progress. This guide walks you through setting up a TRMNL Private Plugin to display data pushed from Ministry Goals.
Note: TRMNL integration is available in Ministry Goals version 1.2 and later. If you don’t see the TRMNL options in Settings, please update to the latest version from the App Store.
What You Need
- A TRMNL device (any model) connected to your TRMNL account
- Ministry Goals installed on your iPhone, iPad, or Mac
- A TRMNL account at usetrmnl.com
Setup Workflow
Step 1: Create a Private Plugin in TRMNL
- Log in to your TRMNL dashboard at usetrmnl.com.
- Navigate to Plugins and search for “Private Plugin.”
- Click New to create a new Private Plugin.
- Give it a name (e.g., “Ministry Goals — Books Read”).
- Set the Strategy to Webhook.
- Click Save. TRMNL will generate a Webhook URL for you.
- Copy the Webhook URL — you’ll need it in the next step.
Step 2: Enable TRMNL in Ministry Goals
- Open Ministry Goals and go to Settings.
- Find the TRMNL Integration section and toggle it on.
- Paste your Webhook URL into the “TRMNL Webhook URL” field. This becomes the default URL for all goals.
Step 3: Enable a Goal for TRMNL
- Open any Goal’s detail view in Ministry Goals.
- Scroll to the TRMNL section and toggle “Send to TRMNL” on.
- (Optional) If you want this goal to use a different Private Plugin, paste a different Webhook URL in the per-goal field. Otherwise it will use the default URL from Settings.
- Tap “Push This Goal Now” to send data immediately, or enable auto-push on app open.
Step 4: Add the Markup Template
Back in your TRMNL Private Plugin, click “Edit Markup.” You’ll see tabs for different layout sizes (Full, Half Horizontal, etc.). Paste the appropriate recipe from the section below into the Full tab. TRMNL uses Liquid templating, so the {{ variable }} placeholders will be replaced with your actual goal data.
Step 5: Add to Your Playlist
- In your TRMNL dashboard, go to Playlists.
- Add your new Private Plugin to your active playlist.
- Your goal data will appear on your TRMNL device the next time it refreshes.
Tips
- One plugin per goal: Each goal pushes to its own webhook URL. If you want to display multiple goals, create multiple Private Plugins and assign each one a different Webhook URL.
- Rate limits: TRMNL allows up to 12 webhook pushes per hour per plugin. Ministry Goals sends data on demand or once on app open, which stays well within this limit.
- Screen won’t update? TRMNL skips re-rendering when the data hasn’t changed. Make sure your goal has new activity logged, then push again.
- Shared markup: If you use the same custom CSS across multiple plugins, put the
<style>block in the Shared tab of the TRMNL editor so it’s available to all layout sizes.
Markup Recipes by Metric Type
Below are ready-to-paste Liquid templates for each metric type in Ministry Goals. Each recipe is designed for the Full layout (800×480). The templates use TRMNL’s framework CSS classes and include inline styles for the custom elements.
Copy the recipe for your metric type and paste it into the Full tab of your Private Plugin’s Markup Editor. Put the <style> block in the Shared tab if you plan to create multiple plugins.
Cumulative Count
Best for: Books read, people contacted, total hours logged — any metric that grows over time toward an optional target.
What it shows: A large hero number in the center with the unit label underneath. When a target is set, a progress bar appears at the bottom of the screen with the percentage positioned above the fill point.
Webhook fields: title, current, target, unit
<style> .hero-value { font-size: 180px; font-weight: 900; line-height: 1; letter-spacing: -4px; font-variant-numeric: tabular-nums; } .bottom-bar { position: absolute; bottom: 0; left: 0; right: 0; padding: 0 24px 20px 24px; } .progress-wrapper { position: relative; } .target-label { font-size: 13px; font-weight: 500; text-align: right; margin-bottom: 6px; } .playhead-label { position: absolute; top: -20px; transform: translateX(-50%); font-size: 13px; font-weight: 700; } .progress-track { width: 100%; height: 12px; background: #e0e0e0; border: 1px solid #000; position: relative; } .progress-fill { height: 100%; background: #000; }</style><div class="layout"> <div class="columns"><div class="column" style="display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center;"> <span class="title">{{ title }}</span> <span class="hero-value">{{ current }}</span> {% if unit and unit != "" %} <span class="label label--small" style="text-transform:uppercase; letter-spacing:2px;">{{ unit }}</span> {% endif %} </div></div></div>{% if target and target > 0 %} {% assign pct = current | times: 100.0 | divided_by: target %} {% if pct > 100 %}{% assign pct = 100 %}{% endif %} <div class="bottom-bar"> <div class="target-label">Target: {{ target }}</div> <div class="progress-wrapper"> <span class="playhead-label" style="left:{{ pct | round }}%;">{{ pct | round }}%</span> <div class="progress-track"> <div class="progress-fill" style="width:{{ pct | round }}%"></div> </div> </div> </div>{% endif %}
Streak
Best for: Daily prayer, Bible reading, exercise — any habit where consecutive days matter.
What it shows: The current streak as a large number with “current” underneath. If the longest streak is greater, it appears in a smaller size alongside. A 14-day heat strip shows recent activity as filled (active) or empty (inactive) blocks.
Webhook fields: title, streak_current, streak_longest, streak_at_risk, recent_days (array of 1s and 0s)
<style> .hero-value { font-size: 180px; font-weight: 900; line-height: 1; letter-spacing: -4px; font-variant-numeric: tabular-nums; } .stat-secondary { font-size: 48px; font-weight: 600; opacity: 0.5; } .stats-row { display: flex; align-items: baseline; gap: 28px; justify-content: center; margin-top: 8px; } .stat-block { display: flex; flex-direction: column; align-items: center; } .stat-label { font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; opacity: 0.5; } .heat-strip { display: flex; gap: 4px; justify-content: center; margin-top: 16px; } .heat-day { width: 28px; height: 20px; border: 1px solid #000; } .heat-day--active { background: #000; } .heat-day--inactive { background: #fff; }</style><div class="layout"> <div class="columns"><div class="column" style="display:flex; flex-direction:column; align-items:center; text-align:center;"> <span class="title">{{ title }}</span> <div class="stats-row"> <div class="stat-block"> <span class="hero-value">{{ streak_current }}</span> <span class="stat-label">current</span> </div> {% if streak_longest > streak_current %} <div class="stat-block"> <span class="stat-secondary">{{ streak_longest }}</span> <span class="stat-label">longest</span> </div> {% endif %} </div> {% if streak_at_risk %} <span style="margin-top:8px; font-weight:700;"> Warning: AT RISK - log today!</span> {% endif %} {% if recent_days and recent_days.size > 0 %} <div class="heat-strip"> {% for day in recent_days %} <div class="heat-day {% if day == 1 %} heat-day--active{% else %} heat-day--inactive{% endif %}"></div> {% endfor %} </div> {% endif %} </div></div></div>
Cadence Count
Best for: Outreach visits per week, classes taught per month — any frequency-based goal within a time period.
What it shows: The current count as a hero number with the period label (e.g., “visits this week”). For targets of 31 or fewer, filled and empty pip dots provide a visual tally. Larger targets show a progress bar at the bottom instead.
Webhook fields: title, current, target, unit, period
<style> .hero-value { font-size: 180px; font-weight: 900; line-height: 1; letter-spacing: -4px; font-variant-numeric: tabular-nums; } .pip-grid { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 16px; } .pip { width: 18px; height: 18px; border-radius: 50%; border: 2px solid #000; } .pip--filled { background: #000; } .pip--empty { background: #fff; }</style><div class="layout"> <div class="columns"><div class="column" style="display:flex; flex-direction:column; align-items:center; text-align:center;"> <span class="title">{{ title }}</span> <span class="hero-value">{{ current }}</span> <span class="label label--small" style="text-transform:uppercase; letter-spacing:2px;"> {{ unit }} {{ period }}</span> {% if target and target > 0 and target <= 31 %} <div class="pip-grid"> {% for i in (1..target) %} {% if i <= current %} <div class="pip pip--filled"></div> {% else %} <div class="pip pip--empty"></div> {% endif %} {% endfor %} </div> {% endif %} </div></div></div>{% if target and target > 31 %} {% assign pct = current | times: 100.0 | divided_by: target %} {% if pct > 100 %}{% assign pct = 100 %}{% endif %} <div class="bottom-bar"> <div class="target-label">Target: {{ target }}</div> <div class="progress-wrapper"> <span class="playhead-label" style="left:{{ pct | round }}%;">{{ pct | round }}%</span> <div class="progress-track"> <div class="progress-fill" style="width:{{ pct | round }}%"></div> </div> </div> </div>{% endif %}
Score Average
Best for: Sermon ratings, self-assessments, satisfaction scores — any metric where you track a running average.
What it shows: The average displayed to one decimal place as the hero number, with “avg score” and the total entry count beneath. A bottom progress bar appears when a target average is set.
Webhook fields: title, average_display, average_raw, target, unit, entry_count
<style> .hero-value { font-size: 180px; font-weight: 900; line-height: 1; letter-spacing: -4px; font-variant-numeric: tabular-nums; }</style><div class="layout"> <div class="columns"><div class="column" style="display:flex; flex-direction:column; align-items:center; text-align:center;"> <span class="title">{{ title }}</span> <span class="hero-value">{{ average_display }}</span> <span class="label label--small" style="text-transform:uppercase; letter-spacing:2px;"> avg {{ unit }}</span> <span class="label" style="margin-top:8px; opacity:0.5;"> {{ entry_count }} entries</span> </div></div></div>{% if target and target > 0 %} {% assign pct = average_raw | times: 100.0 | divided_by: target %} {% if pct > 100 %}{% assign pct = 100 %}{% endif %} <div class="bottom-bar"> <div class="target-label">Target: {{ target }}</div> <div class="progress-wrapper"> <span class="playhead-label" style="left:{{ pct | round }}%;">{{ pct | round }}%</span> <div class="progress-track"> <div class="progress-fill" style="width:{{ pct | round }}%"></div> </div> </div> </div>{% endif %}
Ratio / Percentage
Best for: Visitor return rate, conversion percentages, completion ratios — any metric expressed as a proportion.
What it shows: The value as a hero number. When the unit is set to “percent” or “%”, a percent sign is appended automatically. A bottom progress bar shows progress toward the target.
Webhook fields: title, current, target, unit, is_percent
<style> .hero-value { font-size: 180px; font-weight: 900; line-height: 1; letter-spacing: -4px; font-variant-numeric: tabular-nums; }</style><div class="layout"> <div class="columns"><div class="column" style="display:flex; flex-direction:column; align-items:center; text-align:center;"> <span class="title">{{ title }}</span> {% if is_percent %} <span class="hero-value">{{ current }}%</span> {% else %} <span class="hero-value">{{ current }}</span> <span class="label label--small" style="text-transform:uppercase; letter-spacing:2px;">{{ unit }}</span> {% endif %} </div></div></div>{% if target and target > 0 %} {% assign pct = current | times: 100.0 | divided_by: target %} {% if pct > 100 %}{% assign pct = 100 %}{% endif %} <div class="bottom-bar"> <div class="target-label"> Target: {{ target }}{% if is_percent %}%{% endif %}</div> <div class="progress-wrapper"> <span class="playhead-label" style="left:{{ pct | round }}%;">{{ pct | round }}%</span> <div class="progress-track"> <div class="progress-fill" style="width:{{ pct | round }}%"></div> </div> </div> </div>{% endif %}
Duration
Best for: Prayer time, study sessions, exercise — any time-based goal.
What it shows: The total time formatted as hours and minutes (e.g., “12h 45m”) in a slightly smaller hero font to accommodate the longer string. A bottom progress bar appears when a target is set.
Webhook fields: title, duration_display, duration_target_display, current, target, unit
<style> .hero-value-smaller { font-size: 120px; font-weight: 900; line-height: 1; letter-spacing: -3px; font-variant-numeric: tabular-nums; }</style><div class="layout"> <div class="columns"><div class="column" style="display:flex; flex-direction:column; align-items:center; text-align:center;"> <span class="title">{{ title }}</span> <span class="hero-value-smaller">{{ duration_display }}</span> <span class="label label--small" style="text-transform:uppercase; letter-spacing:2px;">{{ unit }}</span> </div></div></div>{% if target and target > 0 %} {% assign pct = current | times: 100.0 | divided_by: target %} {% if pct > 100 %}{% assign pct = 100 %}{% endif %} <div class="bottom-bar"> <div class="target-label"> Target: {{ duration_target_display }}</div> <div class="progress-wrapper"> <span class="playhead-label" style="left:{{ pct | round }}%;">{{ pct | round }}%</span> <div class="progress-track"> <div class="progress-fill" style="width:{{ pct | round }}%"></div> </div> </div> </div>{% endif %}
Daily Cadence
Best for: “Write 5 days per week,” “Fast 1 day per week” — goals that count the number of qualifying days within a period.
What it shows: Identical layout to Cadence Count. The current count represents qualifying days rather than occurrences, with pip dots for targets of 31 or fewer.
Uses the same Liquid template and webhook fields as Cadence Count above.
Troubleshooting
- Plugin shows old data: TRMNL skips re-rendering when data hasn’t changed. Log new activity in Ministry Goals, then push again.
- Screen says “No data”: Make sure you’ve pushed at least once from Ministry Goals after pasting the markup. Check that your Webhook URL matches what’s in the app’s settings.
- Layout looks broken: Make sure the
<style>block is included either in the markup itself or in the Shared tab. TRMNL’s framework CSS handles the base layout, but the hero-value and progress styles are custom. - JavaScript not working: Wrap any JS in
document.addEventListener("DOMContentLoaded", function(e) { ... });as recommended by TRMNL. - HTTP errors: A 429 means you’ve exceeded 12 pushes per hour for that plugin. Wait a few minutes and try again.
For more help, visit the TRMNL documentation at docs.usetrmnl.com or reach out to us through the Ministry Goals support page.
Leave a comment