OpenAI in WordPress and WooCommerce: Rapidtextai and custom AI tools
AI drafting inside the WordPress editor and WooCommerce admin, built around Rapidtextai. Every output lands as a draft or meta field, never straight on the live site.
- Client
- Content-heavy WordPress and WooCommerce sites (several client deployments plus the Rapidtextai plugin)
- My role
- Plugin author and lead WordPress/AI engineer
- WordPress
- WooCommerce
- OpenAI API
- Rapidtextai
- Action Scheduler
- Block editor
- Embeddings
- PHP
- AI generations per month across client sites
- 8,000+
- SKUs drafted in about 4 hours instead of about 3 weeks
- ≈ 2,500
- monthly API spend cut by per-role caps (first client)
- ≈ 35%
- chat questions answered without a human, CSAT ≈ 4.3/5
- ≈ 62%
AI generations per month across client sites
SKUs drafted in about 4 hours instead of about 3 weeks
monthly API spend cut by per-role caps (first client)
chat questions answered without a human, CSAT ≈ 4.3/5
The challenge
Content teams were buried in product copy and blog briefs. Marketing wanted AI drafting inside the tools they already used, not another SaaS tab to log into.
The hard requirements were brand-voice control, cost guardrails, and output that does not read like a robot. Finance also had to be able to see and limit what the AI was costing.
What I built
- 01
Multi-model routing
One settings screen lets the user pick a model per task: a cheap model for bulk product descriptions, a stronger model for long-form blog drafts, and an image model for featured images. The model list is fetched dynamically, so new API models appear without a plugin update.
- 02
Role-based access and credit budgets
Site admins set a monthly generation cap per role (editor, author, shop manager). Usage is logged per user and per post, with a dashboard showing spend and tokens. This was the feature that got the plugin approved by finance at two client sites.
- 03
Bulk WooCommerce description generator
Action Scheduler runs it in batches of 50 products. Each result is written to a meta field rather than the live product, and a review queue sits in front of publishing.
- 04
Brief-to-draft auto-posting
A topic row in a custom table generates an outline, then the sections, then a draft post with a featured image and SEO meta, flagged for human review. It never publishes directly.
- 05
Editor integration and prompt library
Generate, Rewrite and Expand buttons sit in the block editor toolbar and the classic editor, and a shortcode provides front-end AI forms. Prompts live in the database, are editable per site, and use {{brand_voice}} and {{product_category}} tokens so output stays on-brand.
- 06
Category-aware WooCommerce prompts
In other projects, product descriptions use category-aware prompts and handle variations: generate once per parent, inherit to the variations, with per-variation tweaks. A regenerate action keeps the human-edited version.
- 07
Support chat over the client's docs
Embeddings are stored in a custom table, the top-k matches are retrieved, then a completion call runs with a strict "answer only from context" system prompt and a fallback to a human handoff.
- 08
Images and repurposing
Blog featured images and OG cards are generated with a moderation check before they are sideloaded into the media library. Long posts are repurposed into social captions, a meta description and an email teaser, all written as post meta for review.
Gotchas and how I fixed them
Truncated output looks like success
When the model hits max_tokens the API still returns HTTP 200, so a cut-off description would be saved as if it were finished. Checking finish_reason for "length" and returning an error instead fixes it.
Bulk runs must not overwrite human edits
Generated text goes to a draft meta field, and a _description_human_edited flag makes the job skip any product a person has already touched. A human still has the final click.
Rate limits should queue, not fail
A 429 response schedules a retry through Action Scheduler a minute later instead of failing the request.
Code from the build
function my_ai_completion( string $prompt, string $model = 'gpt-4o-mini', int $max_tokens = 800 ): string|WP_Error {
$body = [
'model' => $model,
'messages' => [
[ 'role' => 'system', 'content' => 'You are a helpful product copywriter. Answer only from the provided context.' ],
[ 'role' => 'user', 'content' => $prompt ],
],
'max_tokens' => $max_tokens,
'temperature' => 0.7,
];
$response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', [
'timeout' => 60,
'headers' => [
'Authorization' => 'Bearer ' . MY_OPENAI_KEY,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( $body ),
] );
if ( is_wp_error( $response ) ) {
return $response;
}
$code = wp_remote_retrieve_response_code( $response );
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( 429 === $code ) {
// Rate limited — schedule a retry rather than failing the request.
as_schedule_single_action( time() + 60, 'my_ai_retry', [ $prompt, $model ] );
return new WP_Error( 'ai_rate_limited', 'Rate limited; queued for retry.' );
}
if ( 200 !== $code ) {
return new WP_Error( 'ai_api_error', $data['error']['message'] ?? 'Unknown API error' );
}
$choice = $data['choices'][0] ?? null;
if ( ! $choice ) {
return new WP_Error( 'ai_empty', 'No completion returned.' );
}
if ( 'length' === ( $choice['finish_reason'] ?? '' ) ) {
return new WP_Error( 'ai_truncated', 'Output hit max_tokens; increase the limit or split the prompt.' );
}
return trim( $choice['message']['content'] );
}The finish_reason check is the most important line, because without it truncated output looks like a successful completion and gets saved as a broken description.
add_action( 'my_generate_product_description', function ( int $product_id ): void {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return;
}
// Never overwrite a human edit.
if ( '1' === get_post_meta( $product_id, '_description_human_edited', true ) ) {
return;
}
$prompt = sprintf(
"Write a 120-word product description for: %s. Category: %s. Price: %s. Tone: %s. Return only the description.",
$product->get_name(),
wp_strip_all_tags( wc_get_product_category_list( $product_id ) ),
$product->get_price(),
get_option( 'my_brand_tone', 'professional and warm' )
);
$result = my_ai_completion( $prompt );
if ( is_wp_error( $result ) ) {
update_post_meta( $product_id, '_ai_description_error', $result->get_error_message() );
return;
}
// Store as a draft meta field; the editor publishes deliberately.
update_post_meta( $product_id, '_ai_description_draft', wp_kses_post( $result ) );
update_post_meta( $product_id, '_ai_description_generated_at', time() );
}, 10, 1 );Writing to a meta draft instead of $product->set_description() is what makes the tool safe to run in bulk, since a human still has the final click.
function my_ai_user_can_generate( int $user_id ): bool {
$cap = (int) get_option( 'my_ai_monthly_cap_' . my_ai_user_role( $user_id ), 100 );
$used = (int) get_user_meta( $user_id, '_ai_generations_' . gmdate( 'Ym' ), true );
return $used < $cap;
}
function my_ai_record_generation( int $user_id ): void {
$key = '_ai_generations_' . gmdate( 'Ym' );
$used = (int) get_user_meta( $user_id, $key, true );
update_user_meta( $user_id, $key, $used + 1 );
}The cap is keyed on year-month, so it resets by itself each month without a cron job.
Results
- 8,000+ AI generations per month across client sites, peaking at ≈ 1.2M tokens per day on the bulk product pipeline.
- ≈ 2,500 SKUs drafted in ≈ 4 hours versus ≈ 3 weeks of copywriting; review time came down to ≈ 15 minutes per 50 products.
- Content team output rose ≈ 3× with the same headcount, and time to first draft for a blog post fell from ≈ 2 hours to ≈ 10 minutes.
- Per-role caps cut monthly API spend by ≈ 35% on the first client after their first month of unconstrained usage.
- The chat widget answered ≈ 62% of inbound questions without human escalation, with CSAT of ≈ 4.3/5 on those conversations.
- Zero published-post incidents from auto-generation, because every AI output lands in draft or meta and never goes straight to publish.
Questions people ask
Can OpenAI write WooCommerce product descriptions in bulk without publishing mistakes?
Yes. The bulk generator runs through Action Scheduler in batches of 50 products and writes each result to a draft meta field, not the live product. A review queue sits before publish, and products a person has already edited are skipped. One run drafted about 2,500 SKUs in about 4 hours.
How do I stop a WordPress AI plugin from running up my OpenAI bill?
Set a monthly generation cap per role (editor, author, shop manager) and log usage per user and per post. On the first client, these caps reduced monthly spend by about 35% after the first month of unconstrained use.
Can I use different AI models for different tasks in WordPress?
Yes. Rapidtextai has one settings screen where you choose a model per task: a cheap model for bulk product descriptions, a stronger one for long blog drafts, and an image model for featured images. The model list is fetched live, so new models show up without a plugin update.
Can an AI chat widget answer questions only from my own documentation?
Yes. Your docs are stored as embeddings in a custom table, the top matches are retrieved, and the completion call uses a strict "answer only from context" system prompt with a fallback to a human. This setup answered about 62% of questions without escalation.
Will AI-generated posts publish themselves on my site?
No. The auto-posting pipeline builds an outline, sections, a featured image and SEO meta, then saves a draft flagged for human review. There have been zero published-post incidents from auto-generation.