166 descriptions generated in 8 minutes. Structured specs, a quality gate, post meta caching, and a WP-CLI batch runner with daily-cap-aware early exit.
Every listing on a dealer’s site has a description field. On most dealer sites, that field contains whatever the salesperson typed in 2 minutes, or worse, nothing. A well-written description helps SEO, helps the buyer understand what they’re looking at, and improves conversion. Writing good descriptions for 166 listings manually would take 40+ hours. Generating them with AI takes 8 minutes.
The Anchor build had 166 active listings. Most descriptions were either missing, copied from the manufacturer spec sheet, or contained the previous owner’s name in them from a bad data import. The inventory system from Module 02 captured structured specs (year, make, model, length, hours, condition, price, features), but that data wasn’t being used in the description.
The AI catalog job reads the structured specs for each unit, generates an engaging description from those facts, and stores it as post meta. It runs once per unit (or on demand when specs change) and caches the result so it doesn’t re-generate on every page load.
The prompt receives structured specs and returns a description that reads like a human wrote it, not like a spec sheet was reformatted:
function [client]_get_description_system_prompt(): string {
return <<<PROMPT
You write compelling listing descriptions for boat inventory. Your descriptions:
- Lead with the most appealing aspect of this specific boat (not generic platitudes)
- Include the key specs naturally in prose (don't list them — weave them in)
- Mention the condition and hours in context, not as a raw data dump
- End with one sentence about the kind of buyer this boat suits
- Are 120–180 words. Not shorter. Not longer.
- Use plain, direct language. No exclamation points. No "amazing" or "stunning."
You receive structured spec data. Write only the description — no intro, no label, no quote marks.
PROMPT;
}
function [client]_get_unit_specs_for_description( int $unit_id ): string {
$specs = [
'Year' => get_post_meta( $unit_id, '[client]_year', true ),
'Make' => get_post_meta( $unit_id, '[client]_make', true ),
'Model' => get_post_meta( $unit_id, '[client]_model', true ),
'Length' => get_post_meta( $unit_id, '[client]_length', true ) . ' ft',
'Hours' => get_post_meta( $unit_id, '[client]_hours', true ),
'Condition' => get_post_meta( $unit_id, '[client]_condition', true ),
'Price' => '$' . number_format( (float) get_post_meta( $unit_id, '[client]_price', true ), 0 ),
'Category' => get_post_meta( $unit_id, '[client]_category', true ),
'Engine' => get_post_meta( $unit_id, '[client]_engine', true ),
'Features' => implode( ', ', (array) get_post_meta( $unit_id, '[client]_features', true ) ),
];
return implode( "\n", array_map(
fn( $k, $v ) => $v ? "{$k}: {$v}" : null,
array_keys( $specs ),
$specs
) );
}
Generated descriptions are validated before being stored. A description that’s too short, too long, or contains suspicious patterns gets flagged for human review:
function [client]_validate_ai_description( string $description, array $specs ): array {
$issues = [];
$word_count = str_word_count( $description );
if ( $word_count < 80 ) {
$issues[] = "Too short ({$word_count} words — minimum 80)";
}
if ( $word_count > 220 ) {
$issues[] = "Too long ({$word_count} words — maximum 220)";
}
// Check that the make appears in the description
$stated_make = strtolower( $specs['make'] ?? '' );
if ( $stated_make && stripos( $description, $stated_make ) === false ) {
$issues[] = "Make '{$stated_make}' not found in generated description";
}
// Flag generic filler phrases
$filler = [ 'stunning', 'amazing', 'don\'t miss', 'priced to sell', 'must see' ];
foreach ( $filler as $phrase ) {
if ( stripos( $description, $phrase ) !== false ) {
$issues[] = "Contains filler phrase: '{$phrase}'";
}
}
return $issues;
}
The batch job runs via WP-CLI for manual triggering or on a scheduled basis for newly added units:
// WP-CLI command: wp [client] generate-descriptions [--force]
function [client]_cli_generate_descriptions( array $args, array $assoc_args ): void {
$force = ! empty( $assoc_args['force'] );
$unit_ids = get_posts( [
'post_type' => '[client]_unit',
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'meta_query' => $force ? [] : [
[
'key' => '[client]_ai_description',
'compare' => 'NOT EXISTS',
],
],
] );
WP_CLI::line( count( $unit_ids ) . ' units to process' );
$system = [client]_get_description_system_prompt();
$ok = $failed = $skipped = 0;
foreach ( $unit_ids as $unit_id ) {
$specs_text = [client]_get_unit_specs_for_description( $unit_id );
$specs_arr = [client]_get_unit_specs_array( $unit_id );
$result = [client]_ai_request( [
'task' => 'description',
'system' => $system,
'cache_system' => true,
'messages' => [
[ 'role' => 'user', 'content' => "Write a listing description:\n\n{$specs_text}" ],
],
] );
if ( ! $result['ok'] ) {
if ( $result['error'] === 'daily_cap' ) {
WP_CLI::warning( 'Daily cap hit — stopping batch. Remaining units will process tomorrow.' );
break;
}
$failed++;
WP_CLI::warning( "Failed: unit {$unit_id} — " . $result['error'] );
continue;
}
$issues = [client]_validate_ai_description( $result['content'], $specs_arr );
if ( ! empty( $issues ) ) {
$skipped++;
WP_CLI::warning( "Quality gate failed for unit {$unit_id}: " . implode( '; ', $issues ) );
update_post_meta( $unit_id, '[client]_ai_description_review', implode( '; ', $issues ) );
continue;
}
update_post_meta( $unit_id, '[client]_ai_description', $result['content'] );
update_post_meta( $unit_id, '[client]_ai_description_date', current_time( 'mysql' ) );
delete_post_meta( $unit_id, '[client]_ai_description_review' );
$ok++;
}
WP_CLI::success( "Done — {$ok} generated, {$failed} failed, {$skipped} quality-flagged." );
}
Structured specs → AI description is a higher-quality process than writing descriptions manually because the AI can’t hallucinate specs that aren’t provided. If the year isn’t in the data, it doesn’t appear in the description. The quality gate catches the cases where something went wrong anyway.
The caching pattern — storing the generated description as post meta — means the AI is called once per unit, not on every page view. Without this, a site with 166 listings would make 166 API calls on every page that lists inventory.
166 descriptions generated in 8 minutes on first batch run. 11 units flagged by the quality gate (9 for missing make in description, 2 for descriptions under 80 words on units with incomplete spec data). All 11 reviewed and approved with minor edits.
Regeneration runs on units when specs change significantly (price drop, hours update). New units added to inventory get descriptions generated automatically via the save_post hook, capped to prevent multiple generations in one session.
Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.
None of this gates a single lesson. The curriculum was free before you got here and it stays that way.
You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.
See GAP Concierge →