How We Automated 90% of Our Agency's Manual Work
When I started my agency three years ago, I thought hiring more developers was the answer to our scaling problems. We'd land a new client, manually set up their project, run the same onboarding steps, copy-paste documentation, and spend hours in Slack answering repetitive questions. By month six, we had four developers doing work that—in hindsight—should have been handled by automation.
It wasn't until I sat down and actually tracked where our time went that I realized: we were spending 40+ hours per week on completely repeatable tasks.
That's when everything changed.
The Turning Point: Auditing Our Workflow
Before automating anything, I needed to see what was actually broken. I spent a week documenting every single task:
- Client onboarding: 4-6 hours per new client
- Environment setup: Creating staging/production configs, database seeding
- Deployment pipeline management: Manual testing checklists, server configurations
- Client reporting: Extracting metrics from 5+ tools, formatting into PDFs
- Support tickets: Answering the same FAQ questions repeatedly
- Code reviews: Generic feedback that should have been automated linting rules
The pattern was clear: high-volume, low-complexity work that had zero business value.
What We Automated (And How)
1. Client Onboarding via Infrastructure-as-Code
Instead of manually spinning up servers and databases, we built an automated onboarding flow using Terraform and custom Node scripts.
#!/bin/bash
# onboard-client.sh - Single command to set up everything
CLIENT_NAME=$1
ENVIRONMENT=$2
# Provision infrastructure
terraform -chdir=./infrastructure apply \
-var="client_name=${CLIENT_NAME}" \
-var="environment=${ENVIRONMENT}"
# Seed database with templates
node scripts/seed-database.js ${CLIENT_NAME}
# Create documentation from templates
node scripts/generate-docs.js ${CLIENT_NAME}
# Set up monitoring/alerts
node scripts/setup-monitoring.js ${CLIENT_NAME}
# Create Slack channel and invite team
node scripts/setup-slack.js ${CLIENT_NAME}
echo "✅ Client ${CLIENT_NAME} fully onboarded in $(date)"
Result: What took 4-6 hours now takes 15 minutes. One command. No manual steps.
2. Automated Deployment Pipelines
We switched from a checklist-based deployment process to a fully automated CI/CD pipeline using GitHub Actions. No more "did you remember to run migrations?"
name: Deploy to Production
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to production
run: |
npm run migrate:latest
npm run deploy:production
- name: Run smoke tests
run: npm run smoke-tests
- name: Notify Slack
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "✅ Production deployment successful"
}
Result: Eliminated deployment errors, reduced deploy time from 45 minutes to 8 minutes, killed our "Friday deploy anxiety."
3. Automated Client Reporting
Our clients loved reports, but they were bleeding us dry. I built a reporting automation that pulls from Google Analytics, Stripe, our internal metrics, and generates a beautiful PDF automatically.
// report-generator.js
const PDFDocument = require('pdfkit');
const fs = require('fs');
async function generateClientReport(clientId) {
const client = await db.clients.findById(clientId);
// Fetch metrics from multiple sources
const [analytics, stripe, customMetrics] = await Promise.all([
fetchGoogleAnalytics(client.analyticsId),
fetchStripeMetrics(client.stripeId),
fetchInternalMetrics(clientId)
]);
const doc = new PDFDocument();
const filename = `reports/${client.name}-${new Date().toISOString().split('T')[0]}.pdf`;
doc.pipe(fs.createWriteStream(filename));
// Build report
doc.fontSize(20).text(`${client.name} - Monthly Report`, 100, 50);
doc.fontSize(12).text(`Generated: ${new Date().toLocaleDateString()}`);
// Add sections
addAnalyticsSection(doc, analytics);
addRevenueSection(doc, stripe);
addPerformanceSection(doc, customMetrics);
doc.end();
// Email the report
await sendEmail(client.email, {
subject: `Your Monthly Report - ${client.name}`,
attachments: [{ path: filename }]
});
console.log(`✅ Report sent to ${client.email}`);
}
// Run every month automatically via cron
schedule.scheduleJob('0 9 1 * *', async () => {
const clients = await db.clients.findActive();
for (const client of clients) {
await generateClientReport(client.id);
}
});
Result: What was a 2-3 hour monthly task per client is now a 30-second automated job. Clients are happier (reports are consistent and on-time), we're happier (hours freed up).
4. FAQ Bot in Slack
We were answering the same questions constantly. Built a simple Slack bot that uses GPT-3.5 to answer common questions by querying our documentation first.
// slack-bot.js
const { App } = require('@slack/bolt');
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET
});
app.message(/how do i|what is|can i/, async ({ message, say, client }) => {
try {
// Search our docs first
const relevantDocs = await searchDocumentation(message.text);
if (relevantDocs.length > 0) {
// We found relevant docs, cite them
await say({
text: `Found this in our docs:`,
blocks: relevantDocs.map(doc => ({
type: 'section',
text: {
type: 'mrkdwn',
text: `*${doc.title}*\n${doc.excerpt}\n<${doc.url}|Read more>`
}
}))
});
} else {
// Fall back to GPT for more nuanced questions
const answer = await queryGPT(message.text);
await say(answer);
}
} catch (error) {
console.error(error);
await say('I'm not sure about that one—tagging a human! 🤖');
}
});
app.start();
Result: 70% of questions now get instant answers. Support team handles only complex issues.
5. Code Review Automation
We set up automated linting, formatting, and code quality checks so humans only review for logic and architecture.
// .eslintrc.js - Catches style issues before review
module.exports = {
extends: ['eslint:recommended', 'plugin:react/recommended'],
rules: {
'no-console': 'warn',
'no-unused-vars': 'error',
'indent': ['error', 2],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'react/prop-types': 'warn'
}
};
Our PR template now focuses developers on the actual logic:
## What does this PR do?
[Describe the change]
## Why are we doing this?
[Explain the business value]
## Testing
[How to test this]
---
✅ Linting/formatting automated
✅ Unit tests automated
✅ Manual review focuses on: logic, architecture, edge cases
Result: Code reviews went from 45 minutes to 15 minutes. Fewer "fix the indentation" comments.
The Numbers
After six months of automation implementation:
- 400+ hours saved per year (10+ weeks)
- 4x faster client onboarding
- Zero deployment errors (down from ~2 per month)
- 30 minutes to generate monthly reports (was 30+ hours)
- 65% reduction in repetitive Slack messages
- 2 developers could do work previously requiring 4
More importantly: developers were actually doing development. Engineering morale improved because nobody was stuck in the automation grind.
What We Learned
Start with auditing. You can't automate what you don't measure. Spend a week documenting everything.
Automate boring things first. Don't try to automate your complex deployment strategy if you haven't automated database seeding yet.
Use boring tools. Bash scripts, GitHub Actions, Terraform—they're not glamorous, but they work reliably. We didn't need a $500/month automation platform.
Keep humans in the loop for decisions. Automation handles repeatability. Humans handle judgment calls.
Document your automation. Your future self will thank you. An automated process that nobody understands is just technical debt.
Key Takeaways
- Audit first: Track what's actually stealing your time before automating
- Start small: Automate one 2-3 hour task to prove the ROI
- Use boring technology: Bash, CI/CD, IaC—proven tools beat flashy ones
- Free people up for creative work: The goal isn't to eliminate jobs; it's to eliminate busywork
- **Compound
Top comments (0)