I absolutely hate generic AI-generated blog posts. You know the ones—they always open with ” digital era” and abuse words like “testament” or “” in every paragraph. If you dump a single prompt into ChatGPT, you get garbage. But if you break the writing process into a multi-step pipeline run by local agents, the output actually becomes usable.
We’re going to build a local AI content marketing agent using Node.js and Ollama. This agent doesn’t just write a post in one go. It researches a topic, drafts a structured outline, writes the sections individually to avoid context limits, and refines the tone. Best of all, because it runs locally on your machine, you won’t pay a single cent in API fees.

The Architecture of a Multi-Step Writing Pipeline
When you ask a single LLM call to write a 1,500-word article, it falls apart. The model loses its train of thought, repeats itself, and rushes the ending. To fix this, we split the writing task among three distinct virtual roles:
- The Researcher: Takes your raw topic, identifies key target audiences, extracts technical concepts, and lists the necessary steps.
- The Outliner: Uses the research data to build a logical header structure (H2s and H3s) and defines what each section must cover.
- The Writer: Takes each section of the outline one by one and writes deep, technical paragraphs.
We did something similar when we built our AI email reply agent with Ollama, but content generation requires handling much larger text payloads and maintaining structural state across steps.
Setting Up Ollama and the Model
First, get Ollama running on your local machine. If you haven’t installed it yet, head over to the Ollama official website and grab the installer for your OS.
Once it’s installed, open your terminal and pull the model. For content writing, I’ve found that Mistral or Llama 3 works best. Llama 3 8B has excellent prose style, so we’ll use that today. Run this command in your terminal:
ollama pull llama3
This downloads the 4.7 GB model to your local drive. Make sure Ollama is running in the background. You can verify it by visiting http://localhost:11434 in your browser; you should see “Ollama is running”.
Initializing the Node.js Project
Create a new directory for your agent and spin up a Node.js project. We’ll use ES Modules for clean import syntax.
mkdir local-content-agent
cd local-content-agent
npm init -y
Now open your package.json file and add "type": "module" to enable ES Modules. Next, install the official Ollama JS library. You can check the ollama package page on npm for updates, but the standard install works perfectly.
npm install ollama dotenv
Your project is now ready. Let’s write the code to orchestrate our content pipeline.
Coding the Content Agent Pipeline
Create a file named agent.js. This script coordinates our three agent steps: research, planning, and writing. We’ll write helper functions for each agent role to keep our code clean and maintainable.
import ollama from 'ollama';
import fs from 'fs/promises'; const MODEL_NAME = 'llama3'; // Helper to call Ollama
async function askOllama(systemPrompt, userPrompt) { try { const response = await ollama.chat({ model: MODEL_NAME, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], options: { temperature: 0.7 } }); return response.message.content; } catch (error) { console.error('Error calling Ollama:', error); throw error; }
} // Step 1: The Researcher Agent
async function runResearcher(topic) { console.log('[-] Researcher agent is analyzing the topic...'); const systemPrompt = `You are an expert technical researcher. Your goal is to analyze the user's topic and provide a detailed research brief. Identify: 1. The target audience and their pain points. 2. Key technical terms that must be explained. 3. A step-by-step logical flow of the solution. Keep your response structured, objective, and dense with facts. No fluff.`; return await askOllama(systemPrompt, `Topic: ${topic}`);
} // Step 2: The Outliner Agent
async function runOutliner(topic, researchBrief) { console.log('[-] Outliner agent is structuring the article...'); const systemPrompt = `You are a content strategist. Use the provided research brief to create a highly detailed article outline. Your outline must contain: - An introduction - 3 to 4 distinct H2 sections covering technical details - A conclusion For each section, write 2-3 bullet points explaining exactly what the writer must cover. Do not write the paragraphs yet.`; const userPrompt = `Topic: ${topic}nnResearch Brief:n${researchBrief}`; return await askOllama(systemPrompt, userPrompt);
} // Step 3: The Writer Agent
async function runWriter(topic, outline) { console.log('[-] Writer agent is drafting the content...'); const systemPrompt = `You are a senior developer-writer. Write a highly engaging, technical blog post based on the outline provided. Writing Rules: - Use a direct, conversational tone. Write like a real human developer who just built this. - Use contractions (don't, won't, it's). - Avoid corporate marketing jargon. - Include clear code examples or step-by-step instructions where appropriate. - Output clean markdown.`; const userPrompt = `Topic: ${topic}nnOutline:n${outline}`; return await askOllama(systemPrompt, userPrompt);
}
This script sets up our three core functions. Now, we need an orchestrator function to tie them together and write the final output to a markdown file on our disk.
async function main() { const topic = "How to build a Redis-backed rate limiter in Node.js"; console.log(`Starting content generation pipeline for: "${topic}"n`); const startTime = Date.now(); try { const research = await runResearcher(topic); console.log('[✓] Research complete.n'); const outline = await runOutliner(topic, research); console.log('[✓] Outline complete.n'); const finalDraft = await runWriter(topic, outline); console.log('[✓] Draft complete.n'); const filename = 'draft.md'; await fs.writeFile(filename, finalDraft, 'utf-8'); const duration = ((Date.now() - startTime) / 1000).toFixed(1); console.log(`Success! Draft saved to ${filename} in ${duration}s.`); } catch (error) { console.error('Pipeline failed:', error); }
} main();
This handles the entire workflow. It runs locally, consumes the outputs sequentially, and writes a clean markdown file. Let’s run it and see how it performs.
Running and Testing the Agent
Fire up the script in your terminal using Node.js:
node agent.js
Depending on your hardware, execution time will vary. On an Apple M-series chip or a modern GPU-enabled Linux box, the process takes anywhere from 45 to 90 seconds. Here’s what my terminal output looked like:
Starting content generation pipeline for: "How to build a Redis-backed rate limiter in Node.js" [-] Researcher agent is analyzing the topic...
[✓] Research complete. [-] Outliner agent is structuring the article...
[✓] Outline complete. [-] Writer agent is drafting the content...
[✓] Draft complete. Success! Draft saved to draft.md in 64.2s.
Open the generated draft.md file. You’ll see a structured, highly technical draft that reads far better than any single-prompt ChatGPT output. This works because the writer agent was forced to work within the strict boundaries of the outline and research built in the previous steps.
How I Fixed the Hallucination Loop Gotcha
When I first ran this setup, the Writer agent got stuck in a nasty infinite loop. It kept repeating the same code block over and over until the context window overflowed. This happened because the model got confused trying to generate both markdown formatting and raw JavaScript code within a single prompt context.
To fix this, I had to do two things:
- Adjust the Ollama system prompt to explicitly state where the code blocks should start and stop.
- Explicitly define the format of the output.
If you face similar issues with long-running tasks, you may want to offload the execution pipeline to a queue. For example, you can implement a delayed job queue with Redis and Node.js to process these heavy AI generations in the background. This keeps your main application responsive if you decide to expose this agent via a web interface.
Expanding Your Local Agent System
Once you have the basic pipeline working, you can expand it. For instance, you could add an automated distribution step. Instead of just writing a markdown file, you could send the draft directly to your email newsletter system. You can easily do this by self-hosting an email platform. Check out our guide on how to self-host Listmonk with Docker Compose to build a completely free, self-hosted newsletter channel for your AI-generated drafts.
You can also create a fourth agent step: a “Formatter Agent” that parses the markdown and converts it into HTML or directly pushes it to a CMS database using REST APIs.
Frequently Asked Questions
Can I run this on a standard laptop without a GPU?
Yes. Ollama runs on CPU-only machines, but it’s painfully slow. A generation that takes 60 seconds on a GPU might take 5 to 10 minutes on an older CPU. If you’re running on a CPU, swap out Llama 3 for smaller models like mistral or phi3.
How do I prevent the model from making up API details?
Feed real documentation directly into the Researcher agent. Instead of letting the model guess based on its training data, pass the raw API documentation text into the researcher’s prompt. This is basic Retrieval-Augmented Generation (RAG), and it works wonders.
How do I handle timeouts in Node.js for long generations?
The official Ollama library uses standard HTTP requests under the hood. If your machine is chugging, the request might time out. You can adjust the connection settings or switch to stream-based responses to handle chunks of data as they come in. Check out the Node.js documentation to learn how to manage long-lived HTTP sockets.
Next Steps for Your Content Automation
Now that you have a functioning content agent running locally, you can apply this agentic workflow to other parts of your stack. For instance, you can use similar logic to qualify inbound leads from your forms. Take a look at our tutorial on how to build a local AI lead scoring agent with Ollama and Node.js to see how to run classification tasks on your local machine.

