diff --git a/Widgets/VolunteerTracker/Assets/screenshot-volunteertracker.png b/Widgets/VolunteerTracker/Assets/screenshot-volunteertracker.png
new file mode 100644
index 0000000..58fad3f
Binary files /dev/null and b/Widgets/VolunteerTracker/Assets/screenshot-volunteertracker.png differ
diff --git a/Widgets/VolunteerTracker/README.md b/Widgets/VolunteerTracker/README.md
new file mode 100644
index 0000000..52bc594
--- /dev/null
+++ b/Widgets/VolunteerTracker/README.md
@@ -0,0 +1,31 @@
+# Volunteer Tracker Widget
+
+The Volunteer Tracker is a Domino’s Pizza Tracker–inspired widget that helps volunteers see exactly where they are in the application process. Just like watching your pizza go from “Order Placed” to “Out for Delivery,” this tracker moves you from “Application Received” to “Ready to Serve!”—minus the melted cheese (unless you’re volunteering in the kitchen 🍕).
+
+## Features
+
+- **Domino’s-Style Progress Bar**: Big, bold, and colorful stages that light up as you complete each requirement.
+- **Stages Include**:
+ 1. **Received** – Your application is in!
+ 2. **Leader Review** – Waiting for approval from your leader.
+ 3. **Background Check** – Show if a background check is required.
+ 4. **Additional Requirements** – Tracks milestones, certifications, and forms.
+ 5. **Ready to Serve!** – All steps complete and you’re good to go.
+- **Helpful Links**: Direct links to complete background checks or forms if they’re still pending.
+- **Contact Info**: Displays the opportunity leader’s name and email for quick help.
+- **No Auth Required**: Just add ?response=[Response_ID]&cid=[Contact_GUID] to the URL this widget is on to load the tracker.
+
+## Configuration
+
+- Modify the stored procedure parameters to reflect your church's setup:
+ - **@BGCWebUrl** - Public web address for completing background checks. The Background_Check_GUID will be appended.
+ - **@FormWebUrl** - Public web location of a Custom Form widget. The Form_GUID will be appended.
+ - **@CheckTypeID** - The default/fallback Background_Check_Type_ID for your church.
+ - **@EnforceVolunteerGroup** - If set to 1, then the widget will _only_ work with Groups with a Type that is set as a Volunteer Group.
+- Create any "Requirement" records for the Group Role associated with the Opportunity, if needed. This widget will show all distinct requirements associated with the Group Role.
+- If no Background Check is set in a requirement record, but the Group Role requires a Background Check, then the default/fallback @CheckTypeID will be used.
+- The "Leader Review" step utilizes the "Response Result" field on the Response record. Blank/Null will leave it in the "Pending" state, "Not Placed" will change the tracker to the "closed" state, and any other "Response Result" will advance the tracker to the next stage.
+
+## Screenshots
+
+
\ No newline at end of file
diff --git a/Widgets/VolunteerTracker/StoredProc/StoredProc.md b/Widgets/VolunteerTracker/StoredProc/StoredProc.md
new file mode 100644
index 0000000..ed9613d
--- /dev/null
+++ b/Widgets/VolunteerTracker/StoredProc/StoredProc.md
@@ -0,0 +1,17 @@
+# Stored Procedure Definition
+
+Use this folder to include any sql scripts designed to install Stored Procedures required for the Csutom Widget to work. Please follow the pattern of including a fully installable script including required MinistryPlatform metadata. Examplesa are included to aid in quickly developing installable Stored Procedure scripts
+
+## Installation Notes
+
+All examples in this repository will install the stored procedure, then check API_Procudures for the relevant metadata that defines the stored procedure. Additionally, install scripts will give the Administrators security role access to the script.
+
+## Standard Parameters
+
+The Custom Widget API will attempt to pass @UserName when the website user is logged into widgets. It is highly recommend that you include the @UserName parameter in all Custom Widget Stored Procedures. If @UserName is not required, you should default this parameter to null. When NOT NULL, the absense of this parameter will cause the stored procedure to fail.
+
+Example:
+
+```
+@UserName nvarchar(75) = null
+```
diff --git a/Widgets/VolunteerTracker/StoredProc/api_custom_VolunteerTrackerWidget_JSON.sql b/Widgets/VolunteerTracker/StoredProc/api_custom_VolunteerTrackerWidget_JSON.sql
new file mode 100644
index 0000000..108ff62
--- /dev/null
+++ b/Widgets/VolunteerTracker/StoredProc/api_custom_VolunteerTrackerWidget_JSON.sql
@@ -0,0 +1,263 @@
+DROP PROCEDURE IF EXISTS [dbo].[api_custom_VolunteerTrackerWidget_JSON]
+GO
+
+SET ANSI_NULLS ON
+GO
+SET QUOTED_IDENTIFIER ON
+GO
+
+-- =============================================
+-- api_custom_VolunteerTrackerWidget_JSON
+-- =============================================
+-- Description: Returns the status of requirements for a response to a volunteer opportunity.
+-- Created at MP Dev Lab 2025 at Perimeter Church in Atlanta, Georgia.
+-- Last Modified: 8/22/2025
+-- Stephan Swinford
+-- =============================================
+
+CREATE PROCEDURE [dbo].[api_custom_VolunteerTrackerWidget_JSON]
+ @DomainID INT,
+ @Username NVARCHAR(75) = NULL,
+ @BGCWebUrl NVARCHAR(255) = 'https://yourchurch.ministryplatform.com/portal/backgroundcheck.aspx?background=',
+ @FormWebUrl NVARCHAR(255) = 'https://www.yourchurch.org/form?id=',
+ @CheckTypeID INT = 1, --Default Background Check Type ID
+ @EnforceVolunteerGroup INT = 0, --Enforce whether associated opportunity group must be a 'volunteer group' or not
+ @ContactGUID NVARCHAR(75),
+ @ResponseID INT
+
+AS
+BEGIN
+ SET NOCOUNT ON;
+
+ -- Verify that the Contact_GUID associated with the Reponse matches the provided Contact GUID
+ IF NOT EXISTS (
+ SELECT 1
+ FROM Contacts C
+ JOIN Participants P ON P.Contact_ID = C.Contact_ID
+ JOIN Responses R ON R.Participant_ID = P.Participant_ID
+ WHERE R.Response_ID = @ResponseID
+ AND C.Contact_GUID = @ContactGUID
+ )
+ BEGIN
+ SELECT [JsonResult] = (SELECT 'Invalid data in URL. Please check your URL and try again.' AS ErrorMessage FOR JSON PATH, WITHOUT_ARRAY_WRAPPER);
+ RETURN;
+ END
+
+ SELECT [JsonResult] =
+ (
+ SELECT
+
+ /* 1) Volunteer Information -> object (embedded without escaping) */
+ JSON_QUERY(
+ COALESCE(
+ (
+ SELECT TOP 1
+ C.Nickname,
+ C.Last_Name,
+ C2.Nickname AS Opportunity_Contact_Nickname,
+ C2.Last_Name AS Opportunity_Contact_Last_Name,
+ C2.Email_Address AS Opportunity_Contact_Email,
+ O.Opportunity_Title,
+ R.Response_Date,
+ CASE WHEN R.Closed = 1 AND ISNULL(RR.Response_Result_ID, 2) = 2 THEN 1 ELSE 0 END AS Closed,
+ ISNULL(RR.Response_Result, 'Pending') AS [Leader_Review],
+ CASE WHEN @EnforceVolunteerGroup = 0 THEN 1 ELSE GT.Volunteer_Group END AS [Is_Volunteer_Group],
+ R.Response_ID,
+ C.Contact_GUID
+ FROM Contacts C
+ JOIN Responses R ON R.Response_ID = @ResponseID
+ JOIN Opportunities O ON O.Opportunity_ID = R.Opportunity_ID
+ JOIN Contacts C2 ON C2.Contact_ID = O.Contact_Person
+ LEFT JOIN Groups G ON G.Group_ID = O.Add_to_Group
+ LEFT JOIN Group_Types GT ON GT.Group_Type_ID = G.Group_Type_ID
+ LEFT JOIN Response_Results RR ON RR.Response_Result_ID = R.Response_Result_ID
+ WHERE R.Response_ID = @ResponseID
+ AND C.Contact_GUID = @ContactGUID
+ ORDER BY C.Contact_ID DESC
+ FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
+ ),
+ N'{}' -- fallback to an empty object if no match
+ )
+ ) AS volunteer_information,
+
+ /* 2) Background Check Requirements -> array */
+ (
+ SELECT DISTINCT
+ BCT.Background_Check_Type AS Background_Check_Type,
+ CASE WHEN (EXISTS
+ (
+ SELECT 1
+ FROM Background_Checks BC
+ WHERE BC.Background_Check_Type_ID = BCT.Background_Check_Type_ID
+ AND BC.Contact_ID = P.Contact_ID
+ AND BC.All_Clear = 1
+ AND BC.Background_Check_Expires > GETDATE()
+ )
+ OR (PR.Background_Check_Type_ID IS NULL AND GR.Background_Check_Required=1 AND EXISTS
+ (
+ SELECT 1
+ FROM Background_Checks BC
+ WHERE BC.Contact_ID = P.Contact_ID
+ AND BC.All_Clear = 1
+ AND BC.Background_Check_Expires > GETDATE()
+ ))
+ )
+ THEN 'Completed'
+ WHEN (EXISTS
+ (
+ SELECT 1
+ FROM Background_Checks BC
+ WHERE BC.Background_Check_Type_ID = BCT.Background_Check_Type_ID
+ AND BC.Contact_ID = P.Contact_ID
+ AND BC.Background_Check_Started >= GetDate()-60
+ AND BC.Background_Check_Submitted IS NOT NULL
+ )
+ OR (PR.Background_Check_Type_ID IS NULL AND GR.Background_Check_Required=1 AND EXISTS
+ (
+ SELECT 1
+ FROM Background_Checks BC
+ WHERE BC.Contact_ID = P.Contact_ID
+ AND BC.Background_Check_Started >= GetDate()-60
+ AND BC.Background_Check_Submitted IS NOT NULL
+ ))
+ ) THEN 'Submitted'
+ ELSE 'Needed' END AS Background_Check_Status,
+ ISNULL((
+ SELECT TOP 1
+ CONCAT(@BGCWebUrl, BC.Background_Check_GUID)
+ FROM Background_Checks BC
+ WHERE BC.Background_Check_Type_ID = BCT.Background_Check_Type_ID
+ AND BC.Contact_ID = P.Contact_ID
+ AND BC.Background_Check_Started >= GetDate()-60
+ AND BC.Background_Check_Submitted IS NULL
+ ORDER BY BC.Background_Check_Started DESC
+ ), '') AS Background_Check_URL
+ FROM Participation_Requirements PR
+ INNER JOIN Group_Roles GR ON GR.Group_Role_ID = PR.Group_Role_ID
+ INNER JOIN Opportunities O ON O.Group_Role_ID = GR.Group_Role_ID
+ INNER JOIN Responses R ON R.Opportunity_ID = O.Opportunity_ID
+ LEFT JOIN Background_Check_Types BCT ON BCT.Background_Check_Type_ID = ISNULL(PR.Background_Check_Type_ID, @CheckTypeID)
+ INNER JOIN Participants P ON P.Participant_ID = R.Participant_ID
+ INNER JOIN Contacts C ON C.Contact_ID = P.Contact_ID
+ WHERE R.Response_ID = @ResponseID
+ AND C.Contact_GUID = @ContactGUID
+ FOR JSON PATH
+ ) AS background_checks,
+
+ /* 3) Milestone Requirements -> array */
+ (
+ SELECT DISTINCT
+ M.Milestone_Title,
+ CASE WHEN EXISTS
+ (
+ SELECT 1
+ FROM Participant_Milestones PM
+ WHERE PM.Milestone_ID = M.Milestone_ID
+ AND PM.Participant_ID = P.Participant_ID
+ AND (DATEADD(MONTH, ISNULL(M.Expires_in_Months, 0), PM.Date_Accomplished) > GETDATE()
+ OR M.Expires_in_Months IS NULL)
+ )
+ THEN 'Completed' ELSE 'Needed' END AS Milestone_Status
+ FROM Participation_Requirements PR
+ INNER JOIN Group_Roles GR ON GR.Group_Role_ID = PR.Group_Role_ID
+ INNER JOIN Opportunities O ON O.Group_Role_ID = GR.Group_Role_ID
+ INNER JOIN Responses R ON R.Opportunity_ID = O.Opportunity_ID
+ INNER JOIN Milestones M ON M.Milestone_ID = PR.Milestone_ID
+ INNER JOIN Participants P ON P.Participant_ID = R.Participant_ID
+ INNER JOIN Contacts C ON C.Contact_ID = P.Contact_ID
+ WHERE R.Response_ID = @ResponseID
+ AND C.Contact_GUID = @ContactGUID
+ FOR JSON PATH
+ ) AS milestones,
+
+ /* 4) Certification Requirements -> array */
+ (
+ SELECT DISTINCT
+ CT.Certification_Type,
+ CASE WHEN EXISTS
+ (
+ SELECT 1
+ FROM Participant_Certifications PC
+ WHERE PC.Certification_Type_ID = CT.Certification_Type_ID
+ AND PC.Participant_ID = P.Participant_ID
+ AND ISNULL(PC.Certification_Expires, GETDATE()) > GETDATE()
+ )
+ THEN 'Completed' ELSE 'Needed' END AS Certification_Status
+ FROM Participation_Requirements PR
+ INNER JOIN Group_Roles GR ON GR.Group_Role_ID = PR.Group_Role_ID
+ INNER JOIN Opportunities O ON O.Group_Role_ID = GR.Group_Role_ID
+ INNER JOIN Responses R ON R.Opportunity_ID = O.Opportunity_ID
+ INNER JOIN Certification_Types CT ON CT.Certification_Type_ID = PR.Certification_Type_ID
+ INNER JOIN Participants P ON P.Participant_ID = R.Participant_ID
+ INNER JOIN Contacts C ON C.Contact_ID = P.Contact_ID
+ WHERE R.Response_ID = @ResponseID
+ AND C.Contact_GUID = @ContactGUID
+ FOR JSON PATH
+ ) AS certifications,
+
+ /* 5) Form Requirements -> array */
+ (
+ SELECT DISTINCT
+ F.Form_Title,
+ CONCAT(@FormWebUrl, F.Form_GUID) AS Form_URL,
+ CASE WHEN EXISTS
+ (
+ SELECT 1
+ FROM Form_Responses FR
+ WHERE FR.Form_ID = F.Form_ID
+ AND FR.Contact_ID = P.Contact_ID
+ AND DATEADD(MONTH, ISNULL(F.Months_Till_Expires, 1), FR.Response_Date) > GETDATE()
+ )
+ THEN 'Completed' ELSE 'Needed' END AS Form_Status
+ FROM Participation_Requirements PR
+ INNER JOIN Group_Roles GR ON GR.Group_Role_ID = PR.Group_Role_ID
+ INNER JOIN Opportunities O ON O.Group_Role_ID = GR.Group_Role_ID
+ INNER JOIN Responses R ON R.Opportunity_ID = O.Opportunity_ID
+ INNER JOIN Forms F ON F.Form_ID = PR.Custom_Form_ID
+ INNER JOIN Participants P ON P.Participant_ID = R.Participant_ID
+ INNER JOIN Contacts C ON C.Contact_ID = P.Contact_ID
+ WHERE R.Response_ID = @ResponseID
+ AND C.Contact_GUID = @ContactGUID
+ FOR JSON PATH
+ ) AS forms
+
+ FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
+ );
+END
+GO
+
+
+-- =============================================
+-- SP MetaData Install
+-- =============================================
+DECLARE @spName NVARCHAR(128) = 'api_custom_VolunteerTrackerWidget_JSON'
+DECLARE @spDescription NVARCHAR(500) = 'Returns the status of requirements for a response to a volunteer opportunity.'
+
+IF NOT EXISTS (
+ SELECT API_Procedure_ID FROM dp_API_Procedures WHERE Procedure_Name = @spName
+)
+BEGIN
+ INSERT INTO dp_API_Procedures (Procedure_Name, Description)
+ VALUES (@spName, @spDescription)
+END
+
+-- Grant to Administrators Role
+DECLARE @AdminRoleID INT = (
+ SELECT Role_ID FROM dp_Roles WHERE Role_Name = 'Administrators'
+)
+
+IF NOT EXISTS (
+ SELECT *
+ FROM dp_Role_API_Procedures RP
+ INNER JOIN dp_API_Procedures AP ON AP.API_Procedure_ID = RP.API_Procedure_ID
+ WHERE AP.Procedure_Name = @spName AND RP.Role_ID = @AdminRoleID
+)
+BEGIN
+ INSERT INTO dp_Role_API_Procedures (Domain_ID, API_Procedure_ID, Role_ID)
+ VALUES (
+ 1,
+ (SELECT API_Procedure_ID FROM dp_API_Procedures WHERE Procedure_Name = @spName),
+ @AdminRoleID
+ )
+END
+GO
diff --git a/Widgets/VolunteerTracker/Template/VolunteerTracker.html b/Widgets/VolunteerTracker/Template/VolunteerTracker.html
new file mode 100644
index 0000000..95d7a19
--- /dev/null
+++ b/Widgets/VolunteerTracker/Template/VolunteerTracker.html
@@ -0,0 +1,676 @@
+{%- comment -%}
+Domino’s-style volunteer tracker with large, boldly colored segments that "light up".
+Top-level inputs expected:
+ - volunteer_information (object)
+ - background_checks (array)
+ - milestones (array)
+ - certifications (array)
+ - forms (array)
+ - leader_review (string: "completed" or "pending")
+ - widgetId (optional), userAuthenticated (optional) — not displayed
+Stages (left → right):
+ 1) Received (always complete)
+ 2) Leader Review (from `leader_review`)
+ 3) Background Check (all background_checks complete OR none => complete)
+ 4) Additional Requirements (all milestones+certifications+forms complete OR none => complete)
+ 5) Ready to Serve! (when 2–4 complete)
+{%- endcomment -%}
+
+{% assign vi = volunteer_information %}
+
+{% comment %} Guard: Check if there is a URL error and only render tracker for volunteer-type groups {% endcomment %}
+{% assign vol_raw = vi["Is_Volunteer_Group"] %}
+{% assign vol_txt = vol_raw | downcase | strip %}
+{% assign is_volunteer = false %}
+{% if vol_raw == 1 or vol_txt == '1' or vol_raw == true or vol_txt == 'true' %}
+ {% assign is_volunteer = true %}
+{% endif %}
+
+{% if ErrorMessage %}
+
This URL contains invalid data. Please check your link and try again.
+{{ vi["Opportunity_Title"] | default: "This opportunity" }} is not configured as a volunteer team. Tracker not available.
+No background check required.
+ {% endif %} ++ Have a question or need help completing requirements? + Contact {{ contact_first }} {{ contact_last }}. +
+