This post is a direct continuation of my earlier blog post on the art of designing an effective AI prompt. That first article focused on how to think about prompts—persona, intent, tone, and constraints. This one moves decisively into implementation. Here, you’ll see how those prompt-design principles translate into a production-grade Salesforce integration using the OpenAI API, with an emphasis on structured outputs, traceability, and control. We’ll cover how to request narratives, key points, sources, and references in a way that Salesforce can actually use, and how parameters like max token usage materially affect processing, cost, and result quality.


Why “AI Text” Is Not Enough in Salesforce

Some Salesforce and OpenAI integrations fail quietly. They technically work, but the output is not what the user expects, or provides erratic results. Long paragraphs get generated, users nod approvingly, and then nothing happens—because the output can’t be parsed, stored cleanly, audited, or automated.

Salesforce is not a chat interface. It’s a transactional system designed for:

  • Automation (Flows, Apex)
  • Reporting and dashboards
  • Compliance and auditability
  • Deterministic workflows

If AI output isn’t structured, it’s noise.

That’s the framing for everything that follows. At a high level we will cover the primary steps required to submit dynamic prompts from Salesforce to OpenAI and get back useful narrative.


Creating the OpenAI API Key (and Handling It Correctly)

The OpenAI API uses bearer-token authentication. You generate an API key in OpenAI’s developer console. From that point forward, the key should be treated exactly like a database password.

Hard truth:

  • If your API key is in Apex, you’ve already failed.
  • If it’s in an LWC, you’ve failed twice.
  • Putting the API key in a object as a masked field is a tiny bit better, but in Salesforce, the correct pattern is:
    • External Credential (encrypted secret storage)
    • Named Credential (runtime endpoint abstraction)

This approach is not optional if you’re building something you intend to ship, package, or scale.


Salesforce Configuration: External Credential + Named Credential

The Named Credential becomes your logical endpoint, for example:

callout:OpenAI_NC/v1/responses

Under the hood:

  • The External Credential stores the OpenAI API key securely.
  • A custom Authorization header is constructed: Authorization: Bearer <API_KEY>
  • Apex never sees the raw key.

This gives you:

  • Secure secret management
  • Environment portability
  • Clean packaging and customer onboarding

Why the Responses API (and Why GPT-5.0 +)

OpenAI’s Responses API is the correct interface for modern integrations. It replaces the older Chat Completions approach and supports:

  • Structured outputs
  • Rich metadata
  • Multi-modal expansion later, if needed

For this implementation, we use GPT-5.2, which introduces important nuances around reasoning and token controls.

A Critical GPT-5.2 Constraint

If you want to explicitly set temperature, GPT-5.2 requires:

reasoning.effort = "none"

If you omit this and still send a temperature, the request may fail. This isn’t academic—it’s a runtime concern you’ll hit immediately if you don’t account for it.


Moving Beyond “Summaries”: Designing for Narrative, Key Points, Sources, and References

If you want AI output to be operational in Salesforce, you must stop asking the model to “summarize” and start asking it to report.

The Four Output Elements That Matter

Narrative
This is the human-readable summary. It’s what users see on the record page, in a Flow screen, or in a Slack notification.

Key Points
These are discrete, atomic facts. They are ideal for:

  • Task creation
  • Alerts
  • Dashboards
  • Decision logic

Sources
Sources describe what information the model was allowed to use. Examples:

  • Opportunity records
  • Case histories
  • User-provided notes
  • External documents

Sources are descriptive, not interpretive.

References
References explicitly tie claims back to sources. This is what gives you:

  • Auditability
  • Explainability
  • Defensibility in regulated environments

If you don’t separate sources from references, you cannot explain why the AI said what it said.


Natural Language vs JSON Structure: Where Most Implementations Go Wrong

You can ask for structure using prose:

“Return a summary with bullet points and references.”

You’ll get something back—but it will drift. Field names will change. Sections will be missing. Automation will break.

The correct approach is to define the output contract explicitly using a strict JSON schema and force the model to comply.

That schema becomes the agreement between:

  • Your Apex code
  • Your UI
  • Your automation
  • The model

Natural language instructions are advisory.
JSON structure is authoritative.


Token Management: max_output_tokens Is Not Just About Cost

Token limits are one of the most misunderstood aspects of OpenAI integrations.

What max_output_tokens Actually Controls

When you set a max token limit, you are controlling:

  • Output length
  • Latency
  • Cost
  • Information prioritization

The model will actively decide what to omit when it runs out of tokens.

Practical Effects

  • Set it too low:
    • Narratives truncate
    • References disappear
    • Strict schemas may cause the call to fail entirely
  • Set it too high:
    • Narratives bloat
    • Cost increases
    • Latency spikes

In practice:

  • Narratives compress first
  • References are the first thing lost if you under-allocate
  • Strict schemas are a feature, not a bug—they prevent partial, misleading output

The right strategy is moderate token limits paired with tight schemas and, when necessary, multiple focused calls instead of one massive one.


Apex Implementation: GPT-5.2 with Strict JSON Output

Below is a example Apex service pattern that:

  • Uses a Named Credential
  • Sets GPT-5.2 parameters correctly
  • Enforces structured JSON output for narrative, key points, sources, and references
public with sharing class OpenAIResponsesService {

    private static final String NC = 'OpenAI_NC';
    private static final String PATH = '/v1/responses';

    public class OpenAIResult {
        @AuraEnabled public String rawJson;
        @AuraEnabled public String outputJson;
    }

    @AuraEnabled
    public static OpenAIResult generateReport(
        String userPrompt,
        String persona,
        String sentiment,
        Decimal temperature,
        Integer maxTokens
    ) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:' + NC + PATH);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');

        Map<String, Object> body = new Map<String, Object>{
            'model' => 'gpt-5.2',
            'reasoning' => new Map<String, Object>{ 'effort' => 'none' },
            'temperature' => temperature == null ? 0.2 : temperature,
            'max_output_tokens' => maxTokens == null ? 800 : maxTokens,
            'input' => new List<Object>{
                new Map<String, Object>{
                    'role' => 'system',
                    'content' =>
                        'You are an enterprise Salesforce assistant. ' +
                        'Persona: ' + persona + '. Sentiment: ' + sentiment +
                        '. Follow the JSON schema exactly.'
                },
                new Map<String, Object>{
                    'role' => 'user',
                    'content' => userPrompt
                }
            },
            'text' => new Map<String, Object>{
                'format' => new Map<String, Object>{
                    'type' => 'json_schema',
                    'strict' => true,
                    'name' => 'salesforce_ai_report',
                    'schema' => new Map<String, Object>{
                        'type' => 'object',
                        'additionalProperties' => false,
                        'required' => new List<Object>{
                            'narrative', 'key_points', 'sources', 'references'
                        },
                        'properties' => new Map<String, Object>{
                            'narrative' => new Map<String, Object>{ 'type' => 'string' },
                            'key_points' => new Map<String, Object>{
                                'type' => 'array',
                                'items' => new Map<String, Object>{ 'type' => 'string' }
                            },
                            'sources' => new Map<String, Object>{
                                'type' => 'array',
                                'items' => new Map<String, Object>{
                                    'type' => 'object',
                                    'required' => new List<Object>{
                                        'source_type', 'identifier', 'description'
                                    },
                                    'properties' => new Map<String, Object>{
                                        'source_type' => new Map<String, Object>{ 'type' => 'string' },
                                        'identifier' => new Map<String, Object>{ 'type' => 'string' },
                                        'description' => new Map<String, Object>{ 'type' => 'string' }
                                    }
                                }
                            },
                            'references' => new Map<String, Object>{
                                'type' => 'array',
                                'items' => new Map<String, Object>{
                                    'type' => 'object',
                                    'required' => new List<Object>{
                                        'source_identifier', 'claim'
                                    },
                                    'properties' => new Map<String, Object>{
                                        'source_identifier' => new Map<String, Object>{ 'type' => 'string' },
                                        'claim' => new Map<String, Object>{ 'type' => 'string' }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        };

        req.setBody(JSON.serialize(body));
        HttpResponse res = new Http().send(req);

        if (res.getStatusCode() < 200 || res.getStatusCode() >= 300) {
            throw new AuraHandledException(res.getBody());
        }

        Map<String, Object> payload =
            (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
        List<Object> output = (List<Object>) payload.get('output');

        String outputJson;
        if (!output.isEmpty()) {
            Map<String, Object> msg = (Map<String, Object>) output[0];
            List<Object> content = (List<Object>) msg.get('content');
            outputJson = (String) ((Map<String, Object>) content[0]).get('text');
        }

        OpenAIResult result = new OpenAIResult();
        result.rawJson = res.getBody();
        result.outputJson = outputJson;
        return result;
    }
}


What You Get Out of This Pattern

  • Predictable, schema-validated JSON
  • Clean separation of narrative, facts, and attribution
  • Token-controlled cost and latency
  • Output that Salesforce can store, automate, and explain

Final Thought

Prompt design gets you better answers.
Structured output design gets you usable systems.

The first post in my AI series was about how to talk to the model, this one is about how to make the model accountable inside Salesforce.

more to come…..

We would love to hear your comments!

Trending