When you run a curated AI resource platform, the content paradox hits fast: you need to produce high-quality, fact-checked, human-edited articles about AI tools — but the AI tooling landscape changes every week. Manual editorial workflows scale poorly when your beat is the entire AI ecosystem.
I built artificialus.com on Astro 6 with EmDash CMS, and the core operational challenge was this: how do you get the rigour of a traditional editorial process — fact-checking, structural editing, human review — without the cycle time of a weekly magazine? As MCP (Model Context Protocol) emerges as the standard for agent-tool communication and AI-generated content saturates every feed, the traditional editorial process needs to evolve.
The answer was a fully agentic editorial pipeline orchestrated by OpenCode. Here is how the agents are wired together — and how you can replicate the pattern for your own content operation.
The Platform: EmDash CMS on Astro 6
EmDash is an Astro-native CMS built on Astro 6 that stores content in SQLite (or libSQL, Cloudflare D1, or PostgreSQL) and exposes a MCP (Model Context Protocol) API for machine-to-machine content management. That last bit — the MCP API — is the key architectural decision.
Every EmDash site ships with an MCP endpoint at /_emdash/api/mcp that speaks the Model Context Protocol natively. This means any MCP-compatible client (OpenCode, Claude Desktop, Cursor) can list collections, read content, create drafts, and update articles directly — no REST API wrappers, no custom webhooks.
The CMS schema for articles is minimal by design:
| Field | Type | Purpose |
|---|---|---|
|
| string | Article headline |
|
| portableText | Rich body (h2, h3, normal, blockquote, code, tables, bullet lists) |
|
| text | Short summary (first paragraph, ≤155 chars) |
|
| image | Hero image |
|
| boolean | Homepage placement flag |
Content is stored as Portable Text — a JSON-based rich text format where each block has a _type, style, and children with optional marks. Block objects like code, table, and image have their own structure outside the block type.
The MCP tools available from the CMS include content_create, content_get, content_update, content_publish, content_list, and schema_get_collection — enough to build a complete publishing workflow without ever opening the admin UI.
The Orchestrator: OpenCode
OpenCode is an AI coding agent framework that supports subagent mode, custom JSON configuration, MCP servers for external tools, and a scheduler plugin. The config lives in a single opencode.json file at the project root:
{
"$schema": "https://opencode.ai/config.json",
"instructions": ["artificialus.MD"],
"mcp": {
"emdash-docs": {
"type": "remote",
"url": "https://docs.emdashcms.com/mcp"
},
"artificialus-cms": {
"type": "remote",
"url": "https://artificialus.com/_emdash/api/mcp",
"headers": {
"Authorization": "Bearer ec_pat_..."
}
}
},
"plugin": ["opencode-scheduler"],
"command": {
"pipeline": {
"description": "Avvia la pipeline editoriale completa per un articolo",
"template": "Avvia la pipeline editoriale per l'articolo {{input}}..."
},
"scout": {
"description": "Avvia article-scout per scoprire nuovi trend",
"template": "Usa l'agente @article-scout per scoprire nuovi trend AI..."
},
"revise": {
"description": "Review rapida dopo modifiche manuali",
"template": "Avvia revisione rapida per l'articolo {{input}}..."
}
},
"agent": {
"article-scout": {
"description": "Scopre nuovi contenuti AI",
"mode": "subagent",
"permission": { "webfetch": "allow", "bash": "deny", "edit": "deny" }
}
}
} Two MCP servers are configured: emdash-docs (read-only documentation at the protocol endpoint) and artificialus-cms (the live CMS API with a bearer token for CRUD operations). The opencode-scheduler plugin handles recurring jobs. Custom commands /pipeline, /scout, and /revise start the editorial flow, content discovery, and quick review.
The agent section defines the first agent — article-scout — as a subagent with restricted permissions. The other eight agents are defined as separate .md files in .opencode/agent/, each with their own frontmatter specifying mode, permissions, and description.
The Nine Specialized Agents
Each agent has one job and clear permission boundaries. No agent has more capability than it needs.
| Agent | Permissions | Core Responsibility |
|---|---|---|
| article-scout | webfetch | Discover AI trends, repos, and news |
| writer | webfetch, edit | Write drafts, apply revisions |
| fact-checker | webfetch | Verify factual claims; cannot edit text |
| senior-editor | none (read-only) | Evaluate structure, flow, clarity |
| humanizer | edit | Remove AI writing patterns |
| content-designer | webfetch, edit | Apply formatting, verify URLs |
| editor-in-chief | none (read-only) | Final sign-off: PUBLISH / REVISE / REJECT |
| cms-publisher | bash, edit | Convert markdown to Portable Text, save as draft |
| editorial-pipeline | all | Orchestrate the state machine |
Three agents show how permission models shape the pipeline.
Fact-checker is the most constrained agent by design. It has webfetch to verify claims against primary sources — official docs, release notes, reputable coverage — but no edit or bash access. Every finding goes into a structured report that the writer must read and apply. Separating detection from correction means every change is documented and traceable; errors cannot slip in during the fix process.
Senior-editor cannot access the web at all. This is intentional: its job is to evaluate structure and flow based on the text alone, not to re-verify facts. The first thing it does is validate that all fact-checker issues were resolved. Then it assesses clarity, tone, and pacing — areas where web access would only be a distraction.
Editorial-pipeline is the orchestrator with full permissions: task access to invoke subagents, bash access to create working directories, edit access to write STATE.md, and webfetch for initial setup. It is the only agent that sees the entire workflow from discovery to CMS draft.
The remaining agents have narrow, well-defined roles:
- Humanizer edits text to remove AI patterns (hedging, formulaic transitions, adverb overuse) without changing facts or structure.
- Content-designer applies formatting — bold, italic, blockquotes, verified links — using webfetch to check URLs before insertion.
- CMS-publisher handles the final conversion from markdown to Portable Text JSON via the
@portabletext/markdownlibrary, then callscontent_create(orcontent_updatefor revisions) via MCP. It never auto-publishes; every save is a draft.
Key insight: The permission model is the most important design constraint here. Narrow permissions are not a limitation — they are a design tool.
The Editorial Pipeline: Step by Step
The pipeline is a state machine managed by the editorial-pipeline orchestrator agent. It follows this flow:
writer → fact-checker (revision loop)
→ senior-editor (revision loop)
→ humanizer
→ content-designer
→ writer (approval)
→ editor-in-chief
→ cms-publisher
A quick review mode (`/revise`) also exists: fact-checker only, then directly to `content_update` on the existing CMS draft — useful for post-publication edits or quick corrections. Visually: arrows from the fact-checker and senior-editor loop back to the writer — the revision loops are the most architecturally interesting part of the design.
When you run /pipeline how-i-built-this, here is the full flow. Run /revise how-i-built-this for a quick pass (fact-check + CMS update only, for post-edit corrections).
Here is what happens during a full pipeline run:
Step 0: Setup
The orchestrator creates drafts/pipeline-how-i-built-this/ and initializes STATE.md:
# Pipeline: How I Built an AI-Powered Editorial Pipeline...
## Status: IN_PROGRESS
## Current Stage: 1
## History
| Stage | Agent | Verdict | Date |
|---|---|---|---|
| 0 | setup | initialized | 2026-05-24 | Step 1: Writer produces the draft
The orchestrator invokes the writer subagent with the discovery brief or outline. The writer produces a complete .md file saved as 01-draft.md.
Step 2: Fact-checker (with revision loop)
The fact-checker reads the draft and verifies every factual claim. It uses webfetch to check against primary sources — official documentation, release notes, reputable coverage. The output is a structured report saved as 02-factcheck.md.
If the verdict is REVISE, the report goes back to the writer for corrections, then the fact-checker re-runs. The loop repeats until PASS or escalation to a human editor.
If the verdict is PASS, the pipeline proceeds.
Step 3: Senior editor (with revision loop)
The senior-editor first validates that all fact-checker issues were resolved. Then it evaluates structure, flow, clarity, tone, and pacing — but never factual claims (that is the fact-checker’s domain). The output is 03-senior-editor.md.
Same revision loop logic applies: PASS proceeds, REVISE goes back to writer for corrections. The loop repeats until PASS or escalation to a human editor.
Step 4: Humanizer
The humanizer line-edits the approved draft to remove AI writing patterns. It produces 04-humanized.md with a change summary listing every modification made and its category (hedging, over-explaining, adverb overuse, etc.).
Step 5: Content designer
The content-designer applies formatting for readability — bold, italic, blockquotes, verified inline links, and a further reading section. It uses webfetch to verify every URL before inserting it. The output is 05-formatted.md.
Step 6: Writer approval
The writer reviews the formatted article to confirm it still reflects the original vision. If the designer introduced phrasing changes or formatting that shifts meaning, the writer can request REVISE. If all looks good, APPROVE moves the pipeline forward.
Step 7: Editor-in-chief
The editor-in-chief reviews the article alongside all prior reports — fact-check findings, senior-editor feedback, humanizer changes, designer formatting. The final verdict can be PUBLISH, REVISE (back to writer for fixes), or REJECT (pipeline archived).
Step 8: CMS publisher
The cms-publisher reads the final .md file, extracts title, excerpt, and body, then runs the conversion script:
NODE=/root/.nvm/versions/node/v24.15.0/bin/node
$NODE .opencode/scripts/md-to-pt.mjs drafts/pipeline-{slug}/06-approved.md The script uses the @portabletext/markdown library — a dedicated, schema-driven converter — to transform markdown into Portable Text JSON. The schema is explicitly configured with all EmDash-supported block types (h1/h2/h3, normal, blockquote, bullet/number lists, strong/em/code/strike-through decorators, link annotations, code and table block objects). The output is a complete JSON payload with title, excerpt, and content array.
Tables are converted to EmDash-native format: _type: "table" with tableRow rows and tableCell cells containing arrays of spans. Code blocks become _type: "code" objects storing the code text.
After conversion, the publisher calls content_create (or content_update during /revise) via the artificialus-cms MCP server with status: "draft", then verifies the save with content_get.
// ## Section heading
{"_type":"block", "style":"h2",
"children":[{"_type":"span", "text":"Section heading"}]}
// Normal paragraph with a link
{"_type":"block", "style":"normal",
"children":[
{"_type":"span", "text":"Read more at "},
{"_type":"span", "text":"OpenCode", "marks":["link-0"]}
],
"markDefs":[
{"_key":"link-0", "_type":"link", "href":"https://opencode.ai"}
]}
// Blockquote
{"_type":"block", "style":"blockquote",
"children":[{"_type":"span", "text":"Quote text"}]}
// Code block
{"_type":"code", "_key":"...", "code":"console.log('hello')"}
// Table (simplified)
{"_type":"table", "hasHeaderRow":true,
"rows":[{"_type":"tableRow",
"cells":[{"_type":"tableCell", "isHeader":true,
"content":[{"_type":"span", "text":"Header"}]}]}]} After a successful save, STATE.md is updated to COMPLETE and the CMS draft URL is logged.
Key Design Decisions
Several constraints shaped how this pipeline works.
English-only content, any-language sources. The article-scout searches for content in any language — English, Italian, Chinese, Japanese — but all curated output on artificialus must be in English. This broadens the discovery surface without compromising the reading experience.
Revision loops with hard limits. The fact-checker and senior-editor stages each allow a maximum of 2 consecutive REVISE iterations before escalating to a human editor. This prevents infinite loops and gives the automated process room to converge.
Only the writer edits text. Fact-checker and senior-editor report findings but cannot modify the draft. Deliberate: separating detection from correction means errors cannot be silently introduced during the fix process, and every change is explicitly documented.
Key insight: Separating detection from correction means errors cannot be silently introduced during the fix process, and every change is explicitly documented.
All CMS saves as “draft”. The cms-publisher never auto-publishes. Every article lands in the CMS as a draft, available for human review at https://artificialus.com/articles/[slug]. This preserves a human-in-the-loop for the final live decision.
Portable Text as the interchange format. Markdown is the pipeline’s internal format (easy for LLMs to produce and parse), but the CMS stores Portable Text JSON. The conversion happens at the last step, in the cms-publisher agent, keeping the intermediate pipeline files human-readable and diff-friendly.
What It Looks Like in Practice
The pipeline directory for this article looks like this:
drafts/pipeline-how-i-built-this/
├── STATE.md
├── 01-draft.md
├── 02-factcheck.md
├── 03-senior-editor.md
├── 04-humanized.md
├── 05-formatted.md
├── 06-approved.md
├── 07-eic.md
└── 08-cms-publish.md STATE.md tracks the entire journey. Right now it is a work in progress at the senior-editor stage:
# Pipeline: How I Built an AI-Powered Editorial Pipeline...
## Status: IN_PROGRESS
## Current Stage: 3
## History
| Stage | Agent | Verdict | Date |
|---|---|---|---|
| 0 | setup | initialized | 2026-05-24 |
| 1 | writer | complete | 2026-05-24 |
| 2 | fact-checker | PASS (1 revision) | 2026-05-24 |
| 3 | senior-editor | **currently running** | 2026-05-24 | Each stage’s output file is a complete, human-readable markdown document. You can audit the pipeline at any point — read what the fact-checker flagged, see how the writer responded, or compare the original draft to the humanized version. The entire editorial process is transparent.
Lessons Learned and Next Steps
The revision loop is the most valuable part of this pipeline. Based on our pipeline runs so far, the fact-checker catches multiple issues per article — incorrect version numbers, broken links, outdated statistics. The senior-editor typically flags several structural improvements. These are the kinds of errors that erode trust when they reach readers, and catching them automatically is a significant quality multiplier.
The agent permission model was more important than I initially expected. When the fact-checker cannot edit text, it forces discipline: every fix is documented and traceable. When the senior-editor cannot access the web, it stays focused on the text in front of it. Narrow permissions are not a limitation — they are a design tool.
The same protocol that lets Claude Desktop query a codebase also lets our editorial pipeline create CMS drafts. There is no REST API to design, no authentication middleware to write, no webhook to configure. The CMS already speaks MCP, and OpenCode already consumes MCP; the two systems integrate at the protocol level, not through custom glue code.
What is next. The opencode-scheduler plugin is configured and ready. The next step is to schedule a weekly content discovery job that runs the article-scout on a cron schedule, producing a batch of article ideas every Monday morning. From there, the pipeline can be kicked off with a single command. The goal is a semi-automated editorial workflow where human judgment happens at the right points — topic selection, final review — and everything else is handled by agents with clearly bounded responsibilities.
Further Reading
- EmDash CMS — MCP Server Reference — The official documentation for EmDash’s MCP server, covering all available tools and collection management
- OpenCode AI — The agentic coding framework used to orchestrate this pipeline
- Model Context Protocol — The open standard that makes agent-to-tool communication possible
- OpenCode Plugins — Documentation for OpenCode plugins including the scheduler for recurring jobs and automated workflows
- Portable Text Specification — The JSON-based rich text format used by EmDash and Sanity CMS
This article is being produced by the pipeline it describes, in real time. As you read this, each stage — writer, fact-checker, senior-editor, humanizer, content-designer, editor-in-chief, and cms-publisher — runs sequentially, with each intermediate file saved to the pipeline directory. The entire process is orchestrated by the editorial-pipeline agent via OpenCode.
No comments yet