Digital adoption at scale
Led onboarding, Pendo guidance, and CX operations across five SaaS platforms for 1,500 employees and 500K+ monthly users — including governance, segmentation, training, and a Center of Excellence.
Customer Experience · Product Adoption · Digital Leadership
Formerly Diversified Programming Ent. LLC
I’m a product and customer experience leader with a background in software development, digital operations, and cross-functional delivery. I turn customer feedback and usage data into clear priorities — then help teams ship the changes that matter.
Most recently I led digital adoption, in-app guidance, and Voice of the Customer work across five SaaS platforms serving 500,000+ monthly users, while building AI-assisted workflows that surface friction and opportunity for product and UX teams.
Led onboarding, Pendo guidance, and CX operations across five SaaS platforms for 1,500 employees and 500K+ monthly users — including governance, segmentation, training, and a Center of Excellence.
Built AI agents that analyze user feedback, match it to Snowflake clickstream data, and score friction points by potential impact so UX Research and product teams know where to dig next.
Product Owner and Scrum Master for the Heinemann.com rebuild — delivered to WCAG 2.1 AA and produced VPAT documentation used by the sales team for enterprise proposals.
Established Heinemann’s SEO and inbound strategy and built an SEO-focused blog that now receives approximately 550,000 visits per month.
Technical notes and utilities from real projects — kept here because they’re still useful, and because the work of improving customer experience sometimes means writing the small pieces that make the experience work.
Evaluates a prompt for any inaccuracies before you actually execute the prompt itself. Paste this into any AI then drop your original prompt at the bottom.
You are an expert AI prompt auditor.
### TASK:
Evaluate the following prompt for:
1. Accuracy – Clear, unambiguous instructions aligned with intended purpose.
2. Grammar & Spelling – Correct language usage, no typos.
3. Completeness – Includes all necessary context, constraints, and expected output format.
4. Format & Structure – Logical flow, modular sections (e.g., Context, Task, Constraints).
5. Dataset Guardrails – Avoids unsafe, biased, or non-compliant content; respects privacy and compliance.
6. Conciseness – Clear and efficient wording without unnecessary verbosity.
7. Tone & Audience Fit – Matches intended audience and purpose.
8. Output Predictability – Likely to yield consistent, high-quality results across runs.
### VALIDATION STEPS:
1. Interpretation – Read and understand the prompt’s intent.
2. Scoring – Rate each category from 1 (poor) to 5 (excellent) and explain reasoning.
3. Issue Identification – List problems by category; classify as Critical or Minor.
4. Risk Assessment – Flag any compliance, bias, or ambiguity risks. Suggest changes to add neutrality and previous conversation history.
5. Improvement Suggestions – Provide actionable fixes for clarity, correctness, and usability.
6. Rewrite – Produce an optimized version of the prompt that meets all criteria.
7. Checklist Confirmation – Output ✓ or ✗ for each category after rewrite.
8. Residual Risks – Highlight any remaining assumptions or limitations.
### OUTPUT FORMAT:
- Summary of Issues (headings and bullet points by category)
- Scorecard (table with category, score, reasoning)
- Risk Flags (if any)
- Sources cited and provide evidence
- Improved Version of the Prompt (fully corrected and optimized)
- Checklist Confirmation (✓ Accuracy, ✓ Grammar, ✓ Completeness, ✓ Format, ✓ Guardrails, ✓ Conciseness, ✓ Tone, ✓ Predictability)
- Residual Risks (if applicable)
Prompt to evaluate:
[INSERT PROMPT HERE]
Inserts an invisible embedded guide at runtime in special situations like when a page section is loaded by Ajax/JS.
(function() {
// 1. Safely extract the dynamic Step ID from Pendo's context
if (typeof step !== 'undefined' && step.id) {
var baseId = "pendo-base-" + step.id;
var pendoRoot = document.getElementById(baseId);
if (pendoRoot) {
// 2. Tear down the layout impact entirely
pendoRoot.style.setProperty('display', 'none', 'important');
pendoRoot.style.setProperty('width', '0px', 'important');
pendoRoot.style.setProperty('height', '0px', 'important');
pendoRoot.style.setProperty('position', 'absolute', 'important');
// 3. Flatten the inner container metrics dynamically
var pendoInner = pendoRoot.querySelector("._pendo-step-container-size");
if (pendoInner) {
pendoInner.style.setProperty('display', 'none', 'important');
pendoInner.style.setProperty('height', '0px', 'important');
pendoInner.style.setProperty('width', '0px', 'important');
}
}
// 4. Fire the metric log and clean up immediately
if (window.pendo && pendo.buildingBlocks && pendo.buildingBlocks.getGuide) {
var guideInstance = pendo.buildingBlocks.getGuide(step.id);
if (guideInstance) {
guideInstance.dismiss();
}
}
}
})();
Set a Pendo badge to be clickable but have it open a new window instead of the popover.
document.querySelector('#pendo-image-badge-727895d0').onclick = function() {
var redirectWindow = window.open('https://hmhco.storylane.io/share/aitools', '_blank');
redirectWindow.location;
};
Finds occurrences of a search string across every character column in the database.
CREATE TABLE #Results (ColumnName nvarchar(370), ColumnValue nvarchar(3630))
SET NOCOUNT ON
DECLARE @TableName nvarchar(256), @ColumnName nvarchar(128), @SearchStr2 nvarchar(110)
SET @TableName = ''
SET @SearchStr2 = QUOTENAME('%' + 'CHANGE_THIS_TO_SEARCH_CRITERIA' + '%','''')
WHILE @TableName IS NOT NULL
BEGIN
SET @ColumnName = ''
SET @TableName = (
SELECT MIN(QUOTENAME(TABLE_SCHEMA) + '.' + QUOTENAME(TABLE_NAME))
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
AND QUOTENAME(TABLE_SCHEMA) + '.' + QUOTENAME(TABLE_NAME) > @TableName
AND OBJECTPROPERTY(OBJECT_ID(QUOTENAME(TABLE_SCHEMA) + '.' + QUOTENAME(TABLE_NAME)), 'IsMSShipped') = 0
)
WHILE (@TableName IS NOT NULL) AND (@ColumnName IS NOT NULL)
BEGIN
SET @ColumnName = (
SELECT MIN(QUOTENAME(COLUMN_NAME))
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = PARSENAME(@TableName, 2)
AND TABLE_NAME = PARSENAME(@TableName, 1)
AND DATA_TYPE IN ('char', 'varchar', 'nchar', 'nvarchar')
AND QUOTENAME(COLUMN_NAME) > @ColumnName
)
IF @ColumnName IS NOT NULL
BEGIN
INSERT INTO #Results
EXEC (
'SELECT ''' + @TableName + '.' + @ColumnName + ''', LEFT(' + @ColumnName + ', 3630)
FROM ' + @TableName + ' (NOLOCK)
WHERE ' + @ColumnName + ' LIKE ' + @SearchStr2
)
END
END
END
SELECT ColumnName, ColumnValue FROM #Results
DROP TABLE #Results
Originally written to help recover from SQL injection attacks. Change @BadText (and optionally @ReplaceWith) then run in Query Analyzer / SSMS.
DECLARE @TableName nvarchar(256), @ColumnName nvarchar(128),
@BadTextFixed nvarchar(110), @SQL nvarchar(4000),
@ReplaceCount int, @BadText nvarchar(120), @ReplaceWith nvarchar(120)
SET @BadText = 'Bad text goes here'
SET @ReplaceWith = ''
SET @TableName = ''
SET @BadTextFixed = QUOTENAME('%' + @BadText + '%','''')
SET @ReplaceCount = 0
-- (Full loop structure continues as on the original site —
-- walks every character column and performs a REPLACE)
Classic CF recordset paging pattern.
<cfparam name="url.startRow" default="1">
<cfparam name="rowsPerPage" default="20">
<cfparam name="currentPage" default="1">
<cfset totalRecords = Getnames.recordcount>
<cfset totalPages = totalRecords / rowsPerPage>
<cfset endRow = (startRow + rowsPerPage) - 1>
<cfif endRow GT totalRecords>
<cfset endRow = totalRecords>
</cfif>
<cfif (totalRecords MOD rowsPerPage) GT 0>
<cfset totalPages = totalPages + 1>
</cfif>
<cfif totalPages gte 2>
<strong>Pages: </strong>
<cfloop from="1" to="#totalPages#" index="i">
<cfset startRow = ((i - 1) * rowsPerPage) + 1>
...
</cfloop>
</cfif>
<cfoutput QUERY="Getnames" startrow="#url.startRow#" maxrows="#rowsPerPage#">
Your record output here
</cfoutput>
<cfloop list="#form.fieldnames#" index="i">
<cfset string = "form.#i#">
#i#: #evaluate(string)#
</cfloop>
Example: count line breaks in an address field.
<cfset foundcount = (len(address) - len(replace(address, '<br>', '', 'all'))) / len('<br>')>
Uses the session tracker to see if the same identifier is active in another session.
<cfparam name="alreadylogged" default="0">
<cfset sessions = application.tracker.getSessionCollection(application.applicationName)>
<cfloop collection="#sessions#" item="sessionkey">
<cfloop collection="#sessions[sessionkey]#" item="keys">
<cfif keys EQ "email" and sessions[sessionkey][keys] EQ form.email>
<cfset alreadylogged = 1>
</cfif>
</cfloop>
</cfloop>
<cfif alreadylogged EQ 1>
<script>alert('You are already logged in on another computer.'); history.back(-1);</script>
<cfabort>
</cfif>
<cfset session.email = trim(form.email)>
Quick CF form + query to add a column.
<cfif isDefined("form.tablename")>
<cfquery name="gettables" datasource="#datasrc#" username="#dbuser#" password="#dbpass#">
ALTER TABLE #form.tablename#
ADD #columnname# #definition#
<cfif len(defaultvalue) GT 0>DEFAULT #defaultvalue#</cfif>
</cfquery>
</cfif>
Small client-side patterns that still come in handy for quick prototyping.
// Simple client-side validation / redirect helpers
function goBack() {
history.back(-1);
}
// Basic form field presence check
function hasValue(el) {
return el && el.value && el.value.trim().length > 0;
}
Many of these utilities came from solving real production problems — SQL injection cleanup, session collision detection, quick schema changes, and in-app guidance edge cases. Most of that work has moved into modern platforms, but a few patterns remain useful when you need something small, dependency-free, and immediate.
Use and adapt freely. No warranty is expressed or implied.