June 24, 2026ยท7 min readยทBy Innovibe

How to Automate Any Repetitive Workflow Using an LLM Pipeline

If you can describe a task in words, you can probably automate it with an LLM. Here's a practical framework for identifying which workflows are worth automating and how to build the pipeline.

AIAutomationLLMProductivity

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:

  1. 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.
  2. Do you do it more than 5 times a week? Below that threshold, the build time won't pay off.
  3. 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.

K
Innovibe
Founder & Technical Lead, Innovibe

Building software for 15+ years. Passionate about AI, system design, and shipping things that work.

Frequently asked questions

Do I need a GPU to run this in production?+

For hosted APIs (OpenAI, Anthropic, Google) โ€” no. You call an HTTPS endpoint. GPUs only matter if you're self-hosting models, which is overkill for most production use cases.

How do I keep LLM costs under control?+

Cache identical prompts aggressively, use the smallest model that meets your quality bar, and set hard token limits per request. A response cache alone can cut costs 40โ€“60% on typical workloads.

What's the difference between fine-tuning and RAG?+

Fine-tuning bakes knowledge into model weights โ€” expensive, slow to update. RAG retrieves context at query time โ€” cheap to update, easier to debug. Use RAG for most production use cases and fine-tune only when you need a very specific tone or format.

How do I handle database migrations safely in production?+

Always run migrations before deploying new code (never after). Use additive-only migrations โ€” add columns with defaults, never rename or drop in the same release. Blue-green deployments make this much safer.

When should I add a Redis cache vs just querying Postgres?+

Add Redis when the same query runs more than ~10 times per second, when query time exceeds 10ms and latency matters, or when you need pub/sub. Don't add it preemptively โ€” optimise the query first.

Building something with AI?

We scope and ship AI features quickly. Let's talk.

Start a Conversation