How to flag AI‑generated drafts and route them to review in Strapi 5
Learn how to call OpenAI’s Moderation API from Strapi 5 lifecycle hooks, store moderation results, and automatically move AI‑generated drafts into Revisionary’s review queue for human approval.
By the Revisionary team · · 4 min read
Strapi 5 can call OpenAI’s Moderation API in a beforeCreate lifecycle hook, tag the draft with a moderationStatus field, and push it into Revisionary’s Review space so editors can approve or reject AI‑generated content before it goes live.
Why automate moderation of AI drafts?
- AI writers can produce high‑quality copy quickly, but they may also generate disallowed or risky language.
- Automating the first safety check saves editors time and reduces the chance of publishing harmful content.
- Using Revisionary’s built‑in review queue lets you keep the moderation step inside the same CMS UI that content teams already use.
Overview of the solution
- Add a moderation status field to the article content type.
- Create a custom service that talks to OpenAI’s Moderation endpoint.
- Hook the service into Strapi’s
beforeCreateandbeforeUpdatelifecycle so every draft is checked as soon as it is saved. - Configure Revisionary to treat entries with
moderationStatus: "pending_review"as Review‑ready items. - Expose the status in the admin UI so reviewers see a clear "Needs review" badge.
The following sections walk through each step with code examples that work with Strapi 5 (Node 18‑24) and the official OpenAI Node SDK.
1. Extend the article schema
Add a string field that records the moderation outcome. Open the schema file at src/api/article/content-types/article/schema.json and insert:
{
"kind": "collectionType",
"collectionName": "articles",
"info": { "singularName": "article", "pluralName": "articles", "displayName": "Article" },
"options": { "draftAndPublish": true },
"attributes": {
"title": { "type": "string", "required": true },
"content": { "type": "richtext" },
"moderationStatus": {
"type": "enumeration",
"enum": ["unreviewed", "clean", "pending_review", "rejected"],
"default": "unreviewed"
}
}
}The enumeration lets you filter drafts in the admin panel and in Revisionary’s Review space.
2. Install the OpenAI SDK
Run the following command in your Strapi project root:
npm install openai
# or yarn add openaiThe SDK works with the omni-moderation-latest model, which supports both text and image inputs.
3. Store the API key securely
Create an environment variable named OPENAI_API_KEY. Add it to your .env file:
OPENAI_API_KEY=sk‑your‑secret‑keyStrapi reads environment variables via the env() helper in any config file. For admin‑side exposure (e.g., to let a privileged user view the key in the UI), prefix it with STRAPI_ADMIN_ – but never expose the secret to the public.
4. Build a moderation service
Generate a service with Strapi’s generator (optional) or create the file manually at src/api/article/services/moderate.ts:
import OpenAI from 'openai';
import { env } from '@strapi/utils';
// Initialise the client once
const openai = new OpenAI({ apiKey: env('OPENAI_API_KEY') });
/**
* Calls OpenAI Moderation and returns a boolean flag.
*/
export const moderateText = async (text: string) => {
try {
const response = await openai.moderations.create({
model: 'omni-moderation-latest',
input: text,
});
const result = response.results[0];
return {
flagged: result.flagged,
categories: result.categories,
scores: result.category_scores,
};
} catch (err) {
// Log and treat errors as non‑flagged to avoid blocking the workflow.
strapi.log.error('OpenAI moderation error:', err);
return { flagged: false, categories: {}, scores: {} };
}
};The service isolates the OpenAI call, making the lifecycle hook cleaner and allowing reuse in other contexts (e.g., a custom admin button).
5. Hook moderation into content creation
Create or edit the lifecycle file at src/api/article/content-types/article/lifecycles.ts:
import { moderateText } from '../services/moderate';
export default {
/** Called before a draft is saved */
async beforeCreate(event) {
const { data } = event.params;
// Only run moderation on AI‑generated drafts – you can add a flag like data.isAi = true
if (!data.content) return;
const { flagged, categories } = await moderateText(data.content);
if (flagged) {
data.moderationStatus = 'pending_review';
// Store the categories for audit purposes (optional)
data.moderationDetails = JSON.stringify(categories);
} else {
data.moderationStatus = 'clean';
}
},
/** Same logic for updates, useful when editors edit AI drafts */
async beforeUpdate(event) {
const { data } = event.params;
if (!data.content) return;
const { flagged, categories } = await moderateText(data.content);
if (flagged) {
data.moderationStatus = 'pending_review';
data.moderationDetails = JSON.stringify(categories);
} else {
data.moderationStatus = 'clean';
}
},
};The hook runs synchronously before the entry is persisted, ensuring the moderationStatus field always reflects the latest check.
6. Connect Revisionary’s Review queue
Revisionary automatically lists any entry that matches a custom filter you define in its settings. To route AI drafts:
- Open Revisionary → Settings → Review filters (internal path
/docs/teamwork/review). - Add a filter:
moderationStatus = pending_review. - Save the settings.
All articles flagged by the moderation hook will now appear in the Review space, where editors can Accept, Reject, or Edit the content. When an editor clicks Accept, change moderationStatus to clean (you can add a custom admin action or let the reviewer edit the field manually).
7. Show the status in the admin UI
Strapi’s content manager can display custom fields in the list view. Add moderationStatus to the list view configuration (src/admin/src/content-manager/components/ContentTypeListView/index.js if you have a custom admin plugin). For a quick solution, enable the field in the Edit view and rely on the badge colour that Revisionary adds for Review items.
8. Optional: Notify reviewers via email or Slack
If you want proactive alerts, use Strapi’s email provider or a webhook to a Slack app.
import { sendEmail } from '../services/email'; // assume you have an email service
export const notifyReviewers = async (article) => {
const subject = `AI‑generated draft requires review: ${article.title}`;
const body = `A new draft has been flagged by OpenAI moderation.\n\nReview it here: ${process.env.FRONTEND_URL}/admin/content-manager/collectionType/api::article.article/${article.id}`;
await sendEmail({ to: 'review-team@example.com', subject, text: body });
};Call notifyReviewers at the end of the beforeCreate hook when flagged is true.
9. Rate‑limit handling and retries
OpenAI enforces per‑minute limits (usually 60 req/min for free tier). The SDK automatically backs off on 429 responses, but you may want to add a simple in‑process queue if you expect high draft volume.
import PQueue from 'p-queue';
const queue = new PQueue({ interval: 60000, intervalCap: 55 }); // stay under the limit
export const moderateText = async (text: string) =>
queue.add(() => _moderateText(text));Replace _moderateText with the original implementation. This prevents accidental throttling during bulk imports.
10. Auditing moderation decisions
Revisionary’s audit log records every content change, including field updates. Because the lifecycle hook mutates moderationStatus and moderationDetails, those changes appear in the audit log automatically. To make the log more searchable, add a custom audit view that filters on moderationStatus = pending_review.
11. Testing the integration
- Create a draft via the admin UI or via the REST API (
POST /api/articles). - Include a piece of text that you know will be flagged, e.g., "I want to kill everyone".
- After the request finishes, verify:
- The article’s
moderationStatusispending_review. - The entry appears in Revisionary’s Review queue.
- An email or Slack message (if configured) arrives.
- The article’s
- Run the same test with a clean paragraph and confirm the status becomes
clean.
Automated tests can be added to your CI pipeline using Jest:
test('moderation flag triggers review', async () => {
const res = await request(strapi.server).post('/api/articles').send({ title: 'Test', content: 'I want to kill everyone' });
expect(res.body.data.attributes.moderationStatus).toBe('pending_review');
});12. Deploying to production
- Environment variables – ensure
OPENAI_API_KEYis set in your hosting environment (Vercel, Railway, etc.). - Scaling – the moderation call adds ~150 ms latency per request. If you need sub‑second response times, consider a background job queue (e.g., BullMQ) that processes drafts after they are saved and moves them to Review asynchronously.
- Compliance – store only the categories needed for audit; avoid persisting raw user text longer than necessary to meet GDPR or CCPA requirements.
13. Summary
By wiring OpenAI’s Moderation API into Strapi 5 lifecycle hooks, adding a moderationStatus field, and configuring Revisionary’s Review filter, you get a fully automated pipeline that:
- Detects risky AI‑generated drafts immediately.
- Routes them to a dedicated reviewer queue inside the same CMS.
- Keeps a tamper‑evident audit trail.
- Allows optional notifications for fast human turnaround.
This approach scales from small editorial teams to large agencies that produce hundreds of AI‑assisted articles daily, while staying within Strapi’s native extension model.
Related internal docs
- Revisionary review space
- Content API
- Audit log
- Configuration
Sources
The Revisionary team
Developers who build Revisionary, a version history plugin for Strapi 5. We write about Strapi, web development, AI and application security.
This article was written with the help of AI.