Most automation articles talk about Zapier triggers and webhook chains. That's fine for structured data. But the workflows that are actually painful โ the ones that eat hours every week โ tend to involve reading, judgment, and writing. Those are exactly the tasks LLMs are good at.
This post is about a practical framework for identifying those workflows and turning them into pipelines.
The three-question filter
Before building anything, ask:
- Can you describe what you do in plain English? If you can write the instructions for a new hire in a paragraph, an LLM can probably follow them.
- Do you do it more than 5 times a week? Below that threshold, the build time won't pay off.
- Is a wrong answer recoverable? Automation fails. If one failure causes a cascade, automate a different thing first.
If all three are yes, you have a candidate.
The anatomy of an LLM pipeline
Every LLM automation is some variation of this:
Input โ Prepare context โ LLM call โ Parse output โ Action
The hard parts are almost never the LLM call. They're:
- Getting the right context into the prompt
- Parsing the output reliably
- Handling failures gracefully
Example: automated RFP triage
Here's a real pattern we've built for clients โ automatically triaging incoming RFP emails to decide which ones are worth bidding on.
Step 1: Collect the input
// Pull unread emails from Gmail (or wherever RFPs land)
async function getNewRFPs(): Promise<Email[]> {
const gmail = await getGmailClient()
const messages = await gmail.users.messages.list({
userId: 'me',
q: 'is:unread subject:(RFP OR tender OR proposal OR bid)',
maxResults: 20
})
return Promise.all(messages.data.messages.map(m => getEmailBody(m.id)))
}
Step 2: Prepare the context
Don't dump the raw email into the prompt. Pre-process it โ strip boilerplate, extract the key sections, inject your company profile.
function buildPrompt(email: Email, companyProfile: string): string {
return `
You are a proposal manager at a software development agency.
Company profile:
${companyProfile}
Evaluate this RFP and return a JSON object with:
- fit_score: 1-5 (5 = perfect fit)
- fit_reason: one sentence explaining the score
- deadline: ISO date string or null
- estimated_value: rough dollar range or null
- recommended_action: "bid" | "skip" | "review"
- key_requirements: array of 3-5 bullet points
RFP content:
---
${email.body.slice(0, 4000)}
---
Return only valid JSON, no explanation.
`
}
Step 3: Call the LLM with structured output
import OpenAI from 'openai'
const client = new OpenAI()
interface RFPEvaluation {
fit_score: number
fit_reason: string
deadline: string | null
estimated_value: string | null
recommended_action: 'bid' | 'skip' | 'review'
key_requirements: string[]
}
async function evaluateRFP(email: Email, profile: string): Promise<RFPEvaluation> {
const response = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: buildPrompt(email, profile) }],
response_format: { type: 'json_object' },
temperature: 0.1 // low temp for consistent structured output
})
return JSON.parse(response.choices[0].message.content!)
}
response_format: { type: 'json_object' } is your best friend for reliable parsing. It forces valid JSON even on long outputs.
Step 4: Take action based on the output
async function processRFPs() {
const rfps = await getNewRFPs()
const profile = readFileSync('./company-profile.md', 'utf-8')
for (const rfp of rfps) {
const evaluation = await evaluateRFP(rfp, profile)
if (evaluation.recommended_action === 'bid') {
await createNotionPage({
title: rfp.subject,
...evaluation,
source_email: rfp.id
})
await sendSlackAlert(`๐ฏ New RFP match (${evaluation.fit_score}/5): ${rfp.subject}`)
}
await markEmailProcessed(rfp.id)
}
}
// Run on a schedule
setInterval(processRFPs, 60 * 60 * 1000) // every hour
Handling failures
Three rules:
1. Always validate LLM output before using it.
function isValidEvaluation(data: unknown): data is RFPEvaluation {
return (
typeof data === 'object' &&
data !== null &&
'fit_score' in data &&
typeof (data as any).fit_score === 'number'
)
}
2. Log everything. You need to know when the model drifted or the input changed shape. Store inputs, outputs, and actions in a table.
3. Build a human review queue. For anything with real consequences, route low-confidence outputs to a human instead of acting automatically. fit_score < 3? Auto-skip. fit_score >= 4? Auto-notify. Everything else? Human queue.
Common workflows worth automating this way
- Email classification and routing โ label, tag, or route incoming messages
- Document summarization โ weekly digest of reports, long threads, meeting notes
- Data extraction โ pull structured fields from unstructured documents (invoices, contracts, forms)
- Content drafting โ first drafts of proposals, reports, or updates based on structured inputs
- Code review comments โ flag common patterns in PRs before a human looks
- Customer support triage โ categorize and draft responses to incoming support tickets
The pattern is always the same: get the input, prepare the context, call the model, parse the output, act on it. Once you've built one pipeline, the second one takes a fraction of the time.
Want to build an LLM automation for your team? Get in touch โ we scope and ship these quickly.