I build tools that streamline day-to-day business operations. My background is in operations management at the frontlines of social services work in NYC.
This portfolio documents my work in architecting internal tools, automating critical workflows, and deploying low-code systems that allow organizations to run smoothly.
The Problem: A new office layout broke sightlines between reception and staff. Reliance on paper sign-ins created safety blindspots and slowed down critical communication about high-needs clients.
The Build: An AppSheet dashboard acting as a "Mission Control" for reception.
The Problem: Court reporting was a manual nightmare. Finding cases, looking up dockets, and drafting emails took ~3 minutes per case. With 30+ cases a day, this cost the team 5-6 hours weekly.
The Build: A "Human-in-the-Loop" automation suite using Google Apps Script.
The Problem: Social workers requested phones via ad-hoc Slack DMs. The sole Office Manager was overwhelmed tracking requests across multiple channels, leading to cognitive overload and lost tickets.
The Build: A centralized ticketing system with automated state management.
findStaffByCustomerAppointment_ that correlates timestamps with booking history to recover lost attribution.SYNC_CURSOR_KEY) to fetch only modified transactions, respecting API rate limits.// From syncSquareToSheet()
// Logic: Batch processing with Idempotency & Staff Recovery
function syncSquareToSheet() {
const props = PropertiesService.getScriptProperties();
const nowIso = new Date().toISOString();
// Resume from last sync cursor
const beginIso = props.getProperty(SYNC_CURSOR_KEY) || isoDaysAgo_(30);
const payments = fetchPaymentsUpdatedSince_(beginIso, nowIso);
// Batch Fetch related Orders to minimize API calls (N+1 problem)
const orderIds = unique_(payments.map(p => p.order_id));
const ordersById = batchRetrieveOrders_(orderIds);
// Heuristic: Pre-fetch booking info to resolve missing staff
const bookingInfo = prefetchBookingInfo_(ordersById);
payments.forEach(p => {
// Transform raw API data into payroll row
const row = buildProcessedRow_(
p, ordersById[p.order_id], catalogInfo, staffById,
customersById, commissionData, bookingInfo
);
// Upsert logic (Update if exists, Append if new)
if (paymentRowIndex[p.id]) {
updates.push({row: paymentRowIndex[p.id], values: row});
} else {
appends.push(row);
}
});
props.setProperty(SYNC_CURSOR_KEY, nowIso);
}
syncIdempotent function that first fetches all existing record IDs, creates a hash map, and creates separate `toInsert` and `toUpdate` batches to minimize API calls.// From syncIdempotent()
// Logic: Batching requests to respect API limits & prevent duplicates
function syncIdempotent(sheetName, tableId, uniqueFieldName) {
// 1. Fetch all existing Airtable records to build a Lookup Map
const existingRecords = fetchAllAirtableRecords(tableId);
const recordMap = new Map();
existingRecords.forEach(r => recordMap.set(r.fields[uniqueFieldName], r.id));
// 2. Diff Local Data vs Remote Data
const {hdr, data} = read(sheetName);
const toInsert = [];
const toUpdate = [];
data.forEach((row) => {
const uniqueVal = row[uniqueColIndex];
const fields = mapRowToFields(row);
if (recordMap.has(uniqueVal)) {
toUpdate.push({ id: recordMap.get(uniqueVal), fields: fields });
} else {
toInsert.push({ fields: fields });
}
});
// 3. Execute Batched API Calls (Max 10 per request)
while (toInsert.length > 0) {
const batch = toInsert.splice(0, 10);
airtablePOST(tableId, { records: batch });
Utilities.sleep(250); // Rate limit backoff
}
}
nextTriggerDate based on the current stage and runs an hourly cron job (`weeklyTrigger`) to check for rows that have matured.generateProgressBar) that updates cell values with emojis (🟩⬜) to give non-technical staff instant visual status of a lead's position in the drip campaign.// From processLead()
// Logic: State machine for drip campaign execution
function processLead(rowIndex, weekStage) {
// 1. Validate Data Integrity
if (!hasRequiredFields(row) || row[IS_PAUSED_INDEX]) return;
// 2. Construct Payload for Direct Mail API
const payload = buildPayload(row);
const webhookUrl = props.getProperty(weekStage + '_WEBHOOK');
// 3. Execute External Action
const response = sendToPostalytics(webhookUrl, payload, authHeader);
if (response.getResponseCode() === 200) {
// 4. Update State Machine
const nextWeek = new Date(now.getTime() + (7 * 24 * 60 * 60 * 1000));
const stageNum = parseInt(weekStage.replace('WEEK_', ''));
// Update Stage, Progress Bar, and Next Trigger Date
sheet.getRange(rowIndex, 2).setValue('Week ' + stageNum);
sheet.getRange(rowIndex, 3).setValue(generateProgressBar(stageNum));
sheet.getRange(rowIndex, 5).setValue(nextWeek);
}
}
// Logic: Watch for Status Change -> Execute Action
function onEdit(e) {
const range = e.range;
const sheet = range.getSheet();
// Only trigger on 'Pipeline Progress' column changes
if (sheet.getName() === 'Application' && range.getColumn() === PIPELINE_COL) {
const status = e.value;
const row = range.getRow();
const candidate = getCandidateData(row);
if (status === 'Invite to Interview') {
// Check Quiet Hours before sending SMS
if (isQuietHours()) {
scheduleMessageForMorning(candidate);
} else {
sendTwilioSMS(candidate.phone, getTemplate('INVITE_SMS'));
}
}
}
}