GAP School Module 07 — AI Layer Lesson 7.3

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 situation

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.


What I did

The description prompt

The prompt receives structured specs and returns a description that reads like a human wrote it, not like a spec sheet was reformatted:

PHP
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 ) ); }

Quality gate

Generated descriptions are validated before being stored. A description that’s too short, too long, or contains suspicious patterns gets flagged for human review:

PHP
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 runner

The batch job runs via WP-CLI for manual triggering or on a scheduled basis for newly added units:

PHP
// 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." ); }

Why it matters

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.


The Anchor build

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.


Do this, not that

  • Send specs, not free text, to the model. A prompt that says “write a description for this boat” with unstructured notes produces unpredictable output. Structured specs produce reliable, verifiable descriptions.
  • Validate generated content before storing it. Minimum word count, make/model presence, filler detection. An incorrect description in the database is worse than no description.
  • Cache the result as post meta. The AI is for generation, not for serving. Generate once, store, serve from the database.
  • Use prompt caching on the system prompt for batch jobs. The system prompt is the same for every unit. Cache it and pay pennies instead of dollars for a 166-unit batch.
  • Run batch jobs via WP-CLI, not via admin UI. CLI output gives real-time feedback, handles timeouts gracefully, and doesn’t have the 30-second HTTP timeout that kills long-running admin requests.
When you’re ready to build

The lessons are yours. When you want it built, we’re here.

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.

We’ll use your email to send you a fast-track to a GAP build and occasional notes on how GAP builds digital sales departments. Lessons stay 100% free — no email required to read any of them. We never share or sell your information. Unsubscribe any time. Privacy policy at gapindustriesllc.com/privacy.html.

Done learning how it’s built? We’ll build it.

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 →