Build a Source-Aware AI Editorial Queue With Next.js and Prisma
This implementation guide shows how to model an AI-assisted editorial queue in Next.js and Prisma with source provenance, draft validation, human approval, scheduling, and auditable publication states.
An autonomous content generator is easy to demonstrate and difficult to trust. A production editorial system needs more than a prompt and a publish button. It needs structured sources, explicit workflow states, validation, human ownership, and a record of how the article reached publication.
This guide describes a small architecture for an AI-assisted editorial queue using Next.js Route Handlers and Prisma. The important design decision is simple: an agent may create and revise a draft, but it cannot approve or publish that draft.
1. Model the editorial state explicitly
A single published boolean cannot represent a real workflow. Separate the lifecycle state from the review decision:
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
slug String @unique
content String
sourceLinks Json?
status String @default("DRAFT")
reviewStatus String @default("PENDING")
reviewedById String? @db.ObjectId
reviewedAt DateTime?
scheduledAt DateTime?
publishedAt DateTime?
authorType String @default("HUMAN")
agentName String?
}
The lifecycle may be draft, scheduled, published, or archived. Review may be pending, in review, changes requested, or approved. Keeping these concepts separate prevents a draft from appearing approved simply because it has a publication date.
2. Store sources as structured data
Do not hide source URLs inside the prompt history. Store them with the article so reviewers and readers can inspect the provenance.
type SourceLink = {
label?: string
url: string
}
At the API boundary, allow only valid HTTP and HTTPS URLs, remove duplicates, and reject malformed entries. A later research service can attach retrieval timestamps, publisher reputation, archived copies, and claim-level citations without changing the basic article route.
3. Treat agent output as untrusted input
An agent request should pass through the same validation as a human request. Normalize strings, constrain enums, limit arrays, sanitize rendered HTML, and reject unexpected fields.
function validateEditorialInput(input: EditorialInput) {
if (!input.title || !input.slug || !input.content) {
return "Title, slug, and content are required"
}
if (["PUBLISHED", "SCHEDULED"].includes(input.status) &&
input.reviewStatus !== "APPROVED") {
return "Human approval is required before publication"
}
return null
}
The guard belongs on the server. A disabled button in the admin interface improves usability, but it does not protect the publication from a direct API request.
4. Separate agent credentials from editor credentials
Future agents should use service accounts with narrow permissions. A research agent may create topic candidates. A drafting agent may create or update drafts. A distribution agent may read published stories. None of those roles should receive approval permissions.
The human review endpoint should derive the reviewer identity from the authenticated session rather than accepting a reviewer ID from the request body.
5. Create a focused Route Handler
A draft endpoint can accept normalized editorial input and assign AI provenance automatically:
export async function POST(request: Request) {
const agent = await requireEditorialAgent(request)
const body = await request.json()
const input = parseEditorialInput(body)
return Response.json(
await prisma.post.create({
data: {
...input,
status: "DRAFT",
reviewStatus: "PENDING",
authorType: "AI",
agentName: agent.name,
},
}),
{ status: 201 },
)
}
Notice that the handler overwrites publication fields rather than trusting the agent. A compromised or confused agent should still be unable to bypass workflow rules.
6. Make review changes visible
The minimum useful audit data includes reviewer, review time, internal notes, article update time, agent identity, and the source list. As the system grows, add immutable revisions so editors can compare the original draft with the approved version.
Review interfaces should surface unsupported claims, missing sources, stale information, duplicate coverage, commercial conflicts, and generated language that sounds certain when the evidence is not.
7. Schedule only approved content
A scheduler should query for records that are both approved and due:
const dueStories = await prisma.post.findMany({
where: {
status: "SCHEDULED",
reviewStatus: "APPROVED",
scheduledAt: { lte: new Date() },
},
})
The publishing job should be idempotent so retries cannot create duplicate distribution events. Update the article and enqueue newsletter or social work using stable event identifiers.
8. Verify the workflow in the seed and CI
A realistic seed should include approved AI-assisted articles, a human reviewer, structured sources, metadata, and authorship provenance. Run the seed twice in CI to prove that it is idempotent, then execute a verification script that checks the expected relationships.
This catches a class of failures that type checking cannot: missing database fields, incompatible indexes, relation errors, invalid seed assumptions, and configuration that works only on one developer machine.
A useful system keeps automation inside editorial boundaries
The goal is not to make the agent look autonomous. The goal is to make the publication dependable. Structured state, visible sources, narrow permissions, and human approval create a foundation that can support more capable agents without giving up accountability.