<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Sitaram Srivatsavai</title>
    <description>The latest articles on Forem by Sitaram Srivatsavai (@sitaram_srivatsavai).</description>
    <link>https://forem.com/sitaram_srivatsavai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3614449%2Fc88b4e13-444f-4b1a-8c27-e19584ccdc9c.png</url>
      <title>Forem: Sitaram Srivatsavai</title>
      <link>https://forem.com/sitaram_srivatsavai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sitaram_srivatsavai"/>
    <language>en</language>
    <item>
      <title>Build Your First MCP Server in 30 Minutes: A Practical Guide for Salesforce Developers</title>
      <dc:creator>Sitaram Srivatsavai</dc:creator>
      <pubDate>Sun, 05 Apr 2026 12:50:02 +0000</pubDate>
      <link>https://forem.com/sitaram_srivatsavai/build-your-first-mcp-server-in-30-minutes-a-practical-guide-for-salesforce-developers-2766</link>
      <guid>https://forem.com/sitaram_srivatsavai/build-your-first-mcp-server-in-30-minutes-a-practical-guide-for-salesforce-developers-2766</guid>
      <description>&lt;p&gt;Model Context Protocol (MCP) is one of those technologies that sounds abstract until you build something with it. Then it clicks: it's a standardized way for AI agents to call external tools, with a governance layer that lets you control exactly which tools the agent can access.&lt;/p&gt;

&lt;p&gt;If you're a Salesforce developer working with Agentforce, MCP matters because it replaces one-off REST integrations with a protocol that's consistent across tools. You register a server, pick the tools you want, and they show up as agent actions — managed the same way as your Flows and Apex actions.&lt;/p&gt;

&lt;p&gt;In this guide, I'll walk through building a working MCP server from scratch, connecting it to Agentforce, and explaining the governance patterns that make it production-ready. The server itself is pure TypeScript — nothing Salesforce-specific — so the skills transfer to any MCP-compatible agent platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MCP Actually Is (In 60 Seconds)
&lt;/h2&gt;

&lt;p&gt;MCP is an open protocol — originally created by Anthropic — that defines how AI agents discover and call external tools. It has two sides:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The server&lt;/strong&gt; advertises a list of tools, each with a name, description, and JSON schema for inputs and outputs. It handles execution when a tool is called.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The client&lt;/strong&gt; (your agent platform — Agentforce, Claude, LangChain, etc.) discovers available tools, presents them to the LLM as callable actions, and routes tool calls to the server.&lt;/p&gt;

&lt;p&gt;The protocol handles discovery, invocation, and response formatting. You focus on the tool logic.&lt;/p&gt;

&lt;p&gt;Why not just build a REST API? You can — and for simple integrations, you probably should. MCP adds value when you have multiple tools that an AI agent selects between dynamically, or when you want a governance layer (allowlists) between the tools and the agent. For a single-purpose integration, REST is simpler. For a tool ecosystem, MCP scales better.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;A mock "payer policy lookup" MCP server with three tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;lookup_formulary_coverage&lt;/strong&gt; — Given a payer, plan, and drug, return coverage status and formulary tier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;get_pa_requirements&lt;/strong&gt; — Return whether prior authorization is required and the criteria&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;get_utilization_rules&lt;/strong&gt; — Return step therapy and quantity limit rules&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These simulate the kind of external data an AI agent in Life Sciences would need during benefits verification. In production, they'd call a real payer API or clearinghouse. For this tutorial, they're backed by a JSON file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set Up the Project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;mcp-payer-server &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;mcp-payer-server
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @modelcontextprotocol/sdk zod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;tsconfig.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Node16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Node16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./src"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"esModuleInterop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/**/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Create the Mock Data
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;src/payer_rules.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"AcmeHealth|Gold Plus|RX-OMNI-10mg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"coverage_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Covered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"formulary_tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tier 4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prior_auth_required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"criteria_summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Confirm labeled indication. Document prior therapy failure. Baseline labs required."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"step_therapy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Must try Drug A before RX-OMNI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"quantity_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30 tablets / 30 days"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"restrictions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Specialty pharmacy required"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"BlueCross|Standard PPO|CardioMax-25mg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"coverage_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Covered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"formulary_tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tier 2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prior_auth_required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"criteria_summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No prior authorization required for Tier 2 formulary drugs."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"step_therapy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"None"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"quantity_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"90 tablets / 90 days"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"restrictions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"None"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"MediPlan|Bronze Essential|RX-OMNI-10mg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"coverage_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Not Covered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"formulary_tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Non-formulary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prior_auth_required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"criteria_summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Drug not on formulary. Patient may appeal for exception."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"step_therapy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"N/A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"quantity_limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"N/A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"restrictions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Non-formulary — appeal process available"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key format is &lt;code&gt;payer|plan|drug&lt;/code&gt;. Three records give you enough to demo a covered/PA-required case, a clean coverage case, and a not-covered case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Build the MCP Server
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;src/server.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;McpServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/sdk/server/mcp.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StdioServerTransport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/sdk/server/stdio.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Load mock data&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payer_rules.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;drug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Create server&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpServer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payer-policy-server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Tool 1: Formulary coverage lookup&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lookup_formulary_coverage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check whether a drug is covered under a specific payer and plan. Returns coverage status, formulary tier, and restrictions.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payer organization name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Plan name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;drug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Drug name and strength&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drug&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drug&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;coverage_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coverage_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;formulary_tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formulary_tier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;restrictions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;restrictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;coverage_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;formulary_tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;restrictions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No matching plan/drug rule found.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Tool 2: Prior authorization requirements&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_pa_requirements&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check whether prior authorization is required and return the criteria summary.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payer organization name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Plan name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;drug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Drug name and strength&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drug&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drug&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;prior_auth_required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;prior_auth_required&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;criteria_summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;criteria_summary&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No PA criteria found for this combination.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Tool 3: Utilization management rules&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_utilization_rules&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Return step therapy requirements and quantity limits for a drug under a specific plan.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payer organization name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Plan name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;drug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Drug name and strength&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drug&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;drug&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;step_therapy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;step_therapy&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;None documented.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;quantity_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;quantity_limit&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;None documented.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Start server&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StdioServerTransport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payer Policy MCP Server running on stdio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build and test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsc
node dist/server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You have a working MCP server with three tools. Each tool has a name, a description (the LLM reads this to decide when to use it), typed input parameters via Zod, and a handler that returns structured JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Connect to Agentforce
&lt;/h2&gt;

&lt;p&gt;This is the Salesforce-specific part. If you're using a different MCP client (Claude Desktop, LangChain, Cursor), skip to the next section — the server is the same regardless of client.&lt;/p&gt;

&lt;p&gt;In Salesforce Setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Register the MCP server.&lt;/strong&gt; Navigate to the MCP server setup, add a new server, and provide the connection details (URL if hosted remotely, or the stdio command if running locally during development).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Allowlist your tools.&lt;/strong&gt; This is the governance step. The server exposes three tools, and you explicitly select which ones the agent can use. If you only want the agent to check coverage but not utilization rules, you allowlist &lt;code&gt;lookup_formulary_coverage&lt;/code&gt; and &lt;code&gt;get_pa_requirements&lt;/code&gt;, and leave &lt;code&gt;get_utilization_rules&lt;/code&gt; off the list.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tools appear in the Asset Library.&lt;/strong&gt; Each allowlisted tool becomes an action, managed alongside your Flow actions, Apex actions, and Prompt Template actions. Same governance model, same testing approach.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add to an agent topic.&lt;/strong&gt; Open your agent in Agentforce Builder, navigate to the relevant topic, and add the MCP actions from the Asset Library. They chain with your other actions like any other step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test in Plan Canvas.&lt;/strong&gt; Run a test prompt and watch the agent discover the tools, select the right one based on the user's request, pass the parameters, and process the response. Plan Canvas shows the full reasoning trace.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 5: Test Without Agentforce (MCP Inspector)
&lt;/h2&gt;

&lt;p&gt;If you don't have Agentforce MCP Support access yet, or you just want to test the server independently, use the MCP Inspector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @modelcontextprotocol/inspector node dist/server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This opens a browser UI where you can see the tools list, call each tool with test inputs, and inspect the responses. It's the fastest way to validate your server before connecting it to any agent platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Allowlists Matter More Than You Think
&lt;/h2&gt;

&lt;p&gt;The allowlist isn't just a configuration step — it's the governance mechanism that makes MCP production-ready.&lt;/p&gt;

&lt;p&gt;Consider this scenario: your MCP server exposes 10 tools because it serves multiple agent use cases. One of those tools writes data back to an external system. Without an allowlist, every agent that connects to this server can invoke every tool — including the write operation.&lt;/p&gt;

&lt;p&gt;With allowlists, you control this per agent topic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Benefits verification agent&lt;/strong&gt; gets: &lt;code&gt;lookup_formulary_coverage&lt;/code&gt;, &lt;code&gt;get_pa_requirements&lt;/code&gt;, &lt;code&gt;get_utilization_rules&lt;/code&gt; (read-only)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enrollment agent&lt;/strong&gt; gets: &lt;code&gt;lookup_formulary_coverage&lt;/code&gt;, &lt;code&gt;submit_pa_request&lt;/code&gt; (read + write, with human confirmation gate)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reporting agent&lt;/strong&gt; gets: &lt;code&gt;get_utilization_rules&lt;/code&gt; only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The principle: give each agent the minimum set of tools it needs. Smaller toolset = more predictable behavior = fewer surprises in production. This matters especially in regulated industries where every tool invocation is potentially auditable, but it's good practice regardless of domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting for Other MCP Clients
&lt;/h2&gt;

&lt;p&gt;The server you just built works with any MCP-compatible client. Here are two common alternatives to Agentforce:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Desktop:&lt;/strong&gt; Add the server to your &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"payer-policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/path/to/dist/server.js"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude will discover the tools automatically and use them when relevant to the conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VS Code (via Copilot or Cline):&lt;/strong&gt; Add to &lt;code&gt;.vscode/mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"payer-policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dist/server.js"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server code doesn't change. The tool contracts don't change. Only the client configuration changes. That's the whole point of a protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying Beyond Local Development
&lt;/h2&gt;

&lt;p&gt;For a POC, running the server locally is fine. For production or shared demos, you need to host it somewhere accessible. Heroku is the common choice for Salesforce-connected MCP servers (Salesforce explicitly supports this via AppLink), but any Node.js host works — AWS Lambda, Railway, Fly.io, a plain VM.&lt;/p&gt;

&lt;p&gt;The key deployment consideration: if you're switching from stdio transport (local) to HTTP/SSE transport (remote), you'll need to swap the transport layer in your server code. The tool logic stays identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You've Built
&lt;/h2&gt;

&lt;p&gt;In about 30 minutes, you've built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A working MCP server with three tools, typed inputs, and structured JSON outputs&lt;/li&gt;
&lt;li&gt;Mock data that covers three test scenarios (covered, not covered, PA required)&lt;/li&gt;
&lt;li&gt;A connection path to Agentforce (or Claude, or VS Code)&lt;/li&gt;
&lt;li&gt;A governance model based on allowlists that controls which tools each agent can access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern scales. When you need a fourth tool — maybe &lt;code&gt;check_appeal_status&lt;/code&gt; or &lt;code&gt;get_copay_accumulator&lt;/code&gt; — you add another &lt;code&gt;server.tool()&lt;/code&gt; block with its schema and handler. The protocol handles discovery and invocation. You focus on the logic.&lt;/p&gt;

&lt;p&gt;And when the mock data needs to become real, you replace the JSON file reads with actual API calls to your payer system or clearinghouse. The tool contracts (names, inputs, outputs) stay the same, so nothing in your agent configuration breaks.&lt;/p&gt;

&lt;p&gt;That's what a good integration protocol buys you: the ability to evolve the implementation without changing the interface.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>salesforce</category>
      <category>ai</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Structured Outputs Are the Contract Your AI Agent Is Missing</title>
      <dc:creator>Sitaram Srivatsavai</dc:creator>
      <pubDate>Thu, 26 Mar 2026 23:47:31 +0000</pubDate>
      <link>https://forem.com/sitaram_srivatsavai/structured-outputs-are-the-contract-your-ai-agent-is-missing-24a</link>
      <guid>https://forem.com/sitaram_srivatsavai/structured-outputs-are-the-contract-your-ai-agent-is-missing-24a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Free-text agent responses look great in demos. They fall apart the moment you try to automate anything downstream. Here's the pattern that fixes it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your AI agent just summarized a payer's response to a benefits verification request. The output reads beautifully:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The patient's coverage appears to be active under the Gold Plus plan. Prior authorization is likely required based on the payer's language, though this isn't entirely clear. The copay seems to be around $50-75, and there may be a quantity limit of 30 tablets per month."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now try writing code that processes that. Does "appears to be active" mean coverage is confirmed? Is prior auth required or not? What's the copay — $50 or $75? And good luck extracting "30 tablets per month" reliably when the next response says "one month supply" or "qty limit: 30 units."&lt;/p&gt;

&lt;p&gt;This is the problem that structured outputs solve. And once I started treating them as the contract between the LLM and the rest of my system, every other design decision got easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Beautiful Text, Useless Automation
&lt;/h2&gt;

&lt;p&gt;I've built AI agents for regulated workflows in Life Sciences — benefits verification, clinical trial site selection, medical inquiry response. In every case, the agent's output needed to drive downstream automation: update a record's status, create follow-up tasks, route exceptions to the right person, trigger the next step in a workflow.&lt;/p&gt;

&lt;p&gt;My first implementations all made the same mistake. I let the LLM return natural language summaries and then tried to parse them. Sometimes I wrote regex. Sometimes I added a second LLM call to "extract the fields." Sometimes I just gave up and had a human re-read the summary and fill in the fields manually.&lt;/p&gt;

&lt;p&gt;Every approach was fragile because I was depending on the LLM being consistent in how it phrases things. And consistency is exactly what LLMs don't guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Define a Schema, Enforce It in the Prompt
&lt;/h2&gt;

&lt;p&gt;A structured output is a JSON object with a defined schema that the LLM must conform to. Instead of asking "summarize this payer response," you ask "extract the following fields from this payer response and return them as JSON."&lt;/p&gt;

&lt;p&gt;Here's what the output contract looks like for a benefits verification summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"coverageConfirmed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priorAuthRequired"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"copayNotes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$50 copay per fill"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deductibleNotes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$500 annual, not yet met"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limitationsNotes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Specialty pharmacy required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"missingInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Effective date not stated"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every field has a type. Every field has a purpose. And the downstream logic is trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is the entire routing logic. No parsing. No interpretation.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missingInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Needs Follow-up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missingInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;createTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Follow up: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that to trying to extract the same decisions from a paragraph of prose. The structured output turned a fragile NLP problem into a simple conditional.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Get the LLM to Comply
&lt;/h2&gt;

&lt;p&gt;The schema alone isn't enough — you need to tell the LLM exactly what you expect. The prompt template has three parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Role and constraints:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a benefits verification analyst. Extract structured data
from the payer response below. Never invent information that isn't
in the response. If a field cannot be determined, use null for
booleans, "Not stated" for strings, and add the missing item to
the missingInfo array.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Context (merge fields from your system):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Patient: Jane Doe
Plan: AcmeHealth Gold Plus
Drug: RX-OMNI 10mg
Payer response: [raw text pasted or ingested here]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Output schema with field descriptions:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ONLY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;JSON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;matching&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;schema:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"coverageConfirmed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;coverage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;active&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;drug/plan?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priorAuthRequired"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;boolean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;payer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;prior&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;auth?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"copayNotes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;copay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Not stated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deductibleNotes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;deductible&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;details&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Not stated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"limitationsNotes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;quantity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;limits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;step&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;therapy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;etc.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"missingInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;fields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;that&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;couldn't&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;be&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;determined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;number&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0-100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;how&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;complete/clear&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;payer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field descriptions inside the schema are doing heavy lifting. They tell the LLM what each field means, what the valid values are, and what to do when information is missing. Without them, you get inconsistent interpretations — one run puts copay info in &lt;code&gt;limitationsNotes&lt;/code&gt;, the next puts it in &lt;code&gt;copayNotes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you're using a platform that supports structured output enforcement natively (OpenAI's &lt;code&gt;response_format&lt;/code&gt;, Salesforce's Prompt Builder structured outputs), use it. If not, the prompt-based approach works well as long as you validate the JSON before processing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Domains, Same Pattern
&lt;/h2&gt;

&lt;p&gt;The schema changes, but the pattern is identical across every use case I've built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clinical trial site feasibility:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enrollmentCapacity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"15-20 patients/year"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"therapeuticExperience"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3 prior Phase III oncology trials"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"regulatoryReadiness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IRB approved, ethics committee pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"riskFlags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Ethics committee approval pending"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"overallScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Conditionally activate"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;riskFlags&lt;/code&gt; array drives automatic task creation — one task per flag. The &lt;code&gt;overallScore&lt;/code&gt; drives routing: 80+ with no flags goes to "Activation Ready," 60-79 goes to "Under Review," below 60 goes to "On Hold." No human reads a paragraph and decides what to do. The schema decides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medical inquiry response:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Based on the Phase III LIBERTY trial..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceDocs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DOC-2024-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DOC-2024-047"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"escalateFlag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nextBestAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send_response"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;nextBestAction&lt;/code&gt; field is the routing mechanism. If the confidence is low or &lt;code&gt;escalateFlag&lt;/code&gt; is true, the workflow opens a collaborative review instead of queuing the response for send. The &lt;code&gt;sourceDocs&lt;/code&gt; array makes the response auditable — a reviewer can verify that the answer is grounded in approved content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Audit Advantage
&lt;/h2&gt;

&lt;p&gt;Structured outputs give you something free-text summaries never can: a complete, machine-readable audit trail.&lt;/p&gt;

&lt;p&gt;Every time my agent runs, I store three things on the record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;AI_Summary_JSON__c&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;raw JSON output (the full structured response)&lt;/span&gt;
&lt;span class="py"&gt;AI_Last_Run_By__c&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;the user who triggered the agent&lt;/span&gt;
&lt;span class="py"&gt;AI_Last_Run_At__c&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;timestamp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six months from now, when compliance asks "why was this case marked as Verified?", I can show them the exact JSON the agent produced, with the exact confidence score and the exact fields it extracted. I can show who triggered it and when. Try doing that with a paragraph of prose that was copy-pasted into a notes field.&lt;/p&gt;

&lt;p&gt;In regulated industries — Life Sciences, healthcare, financial services — this isn't a nice-to-have. It's the difference between "we can demonstrate what happened" and "we think the AI said it was fine."&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to Use Structured Outputs
&lt;/h2&gt;

&lt;p&gt;Structured outputs are the right pattern when the agent's output drives automation. They're the wrong pattern when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user is having a conversation and wants a natural language response&lt;/li&gt;
&lt;li&gt;The task is creative or exploratory (brainstorming, writing, ideation)&lt;/li&gt;
&lt;li&gt;The output is the final product, not an intermediate step in a workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a human is the consumer of the output, natural language is fine. If a system is the consumer, demand a schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With the Schema
&lt;/h2&gt;

&lt;p&gt;If I could go back and give myself one piece of advice before building my first AI agent, it would be this: design the output schema before you write a single prompt.&lt;/p&gt;

&lt;p&gt;The schema forces you to answer the hard questions early. What fields does the downstream system need? What are the valid values? What happens when information is missing? What score threshold triggers which action?&lt;/p&gt;

&lt;p&gt;Every prompt you write, every test you create, every piece of routing logic you build — all of it flows from the schema. It's the contract between the AI and the rest of your system. Get it right first, and everything downstream gets simpler.&lt;/p&gt;

&lt;p&gt;Get it wrong — or skip it and use free text — and you'll spend the next six months writing parsers, debugging inconsistent extractions, and explaining to stakeholders why the agent "sometimes gets it right."&lt;/p&gt;

&lt;p&gt;Define the contract. Enforce it. Version it. Your future self will thank you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>json</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>Real-Time Updates in Salesforce Mobile Apps: A Look at Push Notifications and the Mobile SDK</title>
      <dc:creator>Sitaram Srivatsavai</dc:creator>
      <pubDate>Fri, 26 Dec 2025 00:03:03 +0000</pubDate>
      <link>https://forem.com/sitaram_srivatsavai/real-time-updates-in-salesforce-mobile-apps-a-look-at-push-notifications-and-the-mobile-sdk-34o0</link>
      <guid>https://forem.com/sitaram_srivatsavai/real-time-updates-in-salesforce-mobile-apps-a-look-at-push-notifications-and-the-mobile-sdk-34o0</guid>
      <description>&lt;p&gt;Imagine a field representative updating an Interaction from a lightning record page on their laptop just before heading out. The update saves to Salesforce, but the iPad app they use in the car is offline-first and still shows stale data. The rep opens the app, expects to see their latest changes, and instead sees yesterday’s state. They now have to run a full sync, which can be slow and bandwidth-heavy, just to retrieve a single updated record.&lt;/p&gt;

&lt;p&gt;This is the real problem this article aims to address.&lt;/p&gt;

&lt;p&gt;Traditional Salesforce Mobile SDK apps rely heavily on pull-based sync: the device periodically calls REST APIs and applies filters (territory, briefcase, etc.) to refresh local data. This works, but it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The device doesn’t know when something changed in Salesforce.&lt;/li&gt;
&lt;li&gt;You either poll aggressively (wasting battery and bandwidth), or accept stale data until the next sync.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, we will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look at how Salesforce push notifications and the Mobile SDK fit together.&lt;/li&gt;
&lt;li&gt;Show how to configure a connected app and iOS client to receive push notifications.&lt;/li&gt;
&lt;li&gt;Demonstrate how to send targeted push notifications to specific users from Apex.&lt;/li&gt;
&lt;li&gt;Show how to handle silent push notifications in the mobile app to enable near-real-time, selective sync.&lt;/li&gt;
&lt;li&gt;Conclude with design trade-offs and when this pattern is appropriate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article focuses on behavior and design, not on every detail of every configuration screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Ways to Use Push in a Mobile App
&lt;/h2&gt;

&lt;p&gt;From a mobile client’s point of view, Salesforce push notifications are just APNs notifications with a JSON payload. How you use them depends on the behavior you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visible Notifications (Alerts, Banners, Sounds)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The default pattern people think of is a visible notification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APNs payload includes an aps.alert and possibly a sound.&lt;/li&gt;
&lt;li&gt;iOS shows a banner, plays a sound, and increments the badge count.&lt;/li&gt;
&lt;li&gt;The app can handle taps to navigate to a screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is ideal for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Approvals and tasks (“You have a new approval request”).&lt;/li&gt;
&lt;li&gt;Time-critical alerts (“Appointment starting soon”).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not ideal when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You have many record changes.&lt;/li&gt;
&lt;li&gt;You only care about refreshing data silently, not interrupting the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Silent Notifications (&lt;code&gt;content-available&lt;/code&gt;)&lt;/strong&gt;&lt;br&gt;
A &lt;strong&gt;silent notification&lt;/strong&gt; is one that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses &lt;code&gt;aps.content-available = 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Does not specify an alert, sound, or badge.&lt;/li&gt;
&lt;li&gt;Wake your app in the background so it can run code (briefly) without showing UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an offline-first Salesforce app, this is the interesting case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Salesforce sends a silent push saying “record X changed”.&lt;/li&gt;
&lt;li&gt;iOS wakes the app.&lt;/li&gt;
&lt;li&gt;The app fetches just record X from Salesforce and updates its local store.&lt;/li&gt;
&lt;li&gt;The user sees up-to-date data the next time they open the app, without having seen any banners.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the rest of the article, we focus on this pattern: using &lt;strong&gt;silent push as a sideband trigger&lt;/strong&gt; for selective sync.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Configure Push on the Salesforce Side
&lt;/h2&gt;

&lt;p&gt;To send push notifications from Salesforce to a Mobile SDK app, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Connected App&lt;/strong&gt; representing the iOS app.&lt;/li&gt;
&lt;li&gt;APNs credentials for that app (Auth Key or certificate).&lt;/li&gt;
&lt;li&gt;Push is enabled on the connected app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At a high level:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In the Apple Developer portal:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create an App ID with your bundle identifier (e.g. &lt;code&gt;com.company.agent.ipad&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Push Notifications&lt;/strong&gt; for that App ID.&lt;/li&gt;
&lt;li&gt;Create an &lt;strong&gt;APNs Auth Key&lt;/strong&gt; (&lt;code&gt;.p8&lt;/code&gt;) with APNs enabled.&lt;/li&gt;
&lt;li&gt;Note the &lt;strong&gt;Key ID&lt;/strong&gt; and &lt;strong&gt;Team ID&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In Salesforce Setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open your Connected App (used by the Mobile SDK).&lt;/li&gt;
&lt;li&gt;In the iOS push configuration:
-- Upload the &lt;code&gt;.p8&lt;/code&gt; as &lt;strong&gt;Signing Key&lt;/strong&gt;.
-- Enter the &lt;strong&gt;Key Identifier&lt;/strong&gt; and &lt;strong&gt;Team Identifier&lt;/strong&gt;.
-- Set &lt;strong&gt;Bundle ID&lt;/strong&gt; to match the iOS app’s bundle id exactly.
-- Choose the correct environment (Sandbox for dev builds).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Save the connected app and verify there are no errors.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once configured correctly, Salesforce can call APNs on behalf of this app. If you saw errors like “provider token is not valid” or “pushing to this topic is not allowed” during setup, they almost always traced back to mismatched Team ID / bundle id / APNs key.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Register Devices with the Salesforce Mobile SDK
&lt;/h2&gt;

&lt;p&gt;When you run the iOS app on a real device, the app must:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask the user for notification permission.&lt;/li&gt;
&lt;li&gt;Register with APNs to obtain a device token.&lt;/li&gt;
&lt;li&gt;Register that token with Salesforce through the Mobile SDK.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simplified pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import UserNotifications
import SalesforceSDKCore

@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -&amp;gt; Bool {
        UNUserNotificationCenter.current().delegate = self
        registerForRemotePushNotifications()
        return true
    }

    private func registerForRemotePushNotifications() {
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if granted {
                DispatchQueue.main.async {
                    UIApplication.shared.registerForRemoteNotifications()
                }
            } else {
                // Log or handle denial
            }
        }
    }

    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        // Forward the token to Salesforce Mobile SDK
        SFPushNotificationManager.sharedInstance()
            .didRegisterForRemoteNotifications(withDeviceToken: deviceToken)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once this runs after a successful Mobile SDK login, Salesforce creates a &lt;code&gt;MobilePushServiceDevice&lt;/code&gt; record for that &lt;strong&gt;user + connected app + device&lt;/strong&gt;. You can verify this with a simple SOQL query, as you already do in your setup docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Send a Test Push to a Specific User
&lt;/h2&gt;

&lt;p&gt;With the connected app and registration in place, you can send a &lt;strong&gt;targeted push&lt;/strong&gt; from Apex by specifying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The connected app name.&lt;/li&gt;
&lt;li&gt;The user Id (or multiple user Ids).&lt;/li&gt;
&lt;li&gt;The JSON payload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A basic visible test (used during setup) looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;String appName = 'Real_time_Sync'; // Connected App API name
String userId  = 'USER_SALESFORCE_ID';    // Test user Id

Messaging.PushNotification msg = new Messaging.PushNotification();

Map&amp;lt;String, Object&amp;gt; payload = Messaging.PushNotificationPayload.apple(
    'Test Push Notification',
    'default',
    1,
    new Map&amp;lt;String, Object&amp;gt;{ 'type' =&amp;gt; 'TEST', 'message' =&amp;gt; 'Hello from Salesforce' }
);

msg.setPayload(payload);

Set&amp;lt;String&amp;gt; users = new Set&amp;lt;String&amp;gt;{ userId };

try {
    msg.send(appName, users);
    System.debug('Push notification sent successfully!');
} catch (Exception e) {
    System.debug('Push failed: ' + e.getMessage());
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful to confirm that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APNs' credentials are valid.&lt;/li&gt;
&lt;li&gt;Device is registered.&lt;/li&gt;
&lt;li&gt;The app is receiving notifications and logging them.
Once that works, you can switch to silent notifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Send Silent, Data-Oriented Push Notifications
&lt;/h2&gt;

&lt;p&gt;To avoid spamming the user, you convert the payload to a &lt;strong&gt;silent&lt;/strong&gt; notification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;alert&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;sound&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aps.content-available = 1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Custom keys at the root for your app (e.g. type, recordIds).
A simplified example for Account updates:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger accountAlert on Account (after update) {

    final String TARGET_USER_ID = 'TEST_USER_ID'; // Example user

    for (Account acc: Trigger.New) {

        Account oldAcc = Trigger.oldMap.get(acc.Id);

        Boolean nameChanged     = acc.Name     != oldAcc.Name;
        Boolean ownerChanged    = acc.OwnerId  != oldAcc.OwnerId;
        Boolean typeChanged     = acc.Type     != oldAcc.Type;

        if (nameChanged || ownerChanged || typeChanged) {

            List&amp;lt;String&amp;gt; changes = new List&amp;lt;String&amp;gt;();
            if (nameChanged)  changes.add('Name');
            if (ownerChanged) changes.add('Owner');
            if (typeChanged)  changes.add('Type');

            String changedFields = String.join(changes, ', ');

            // APS for silent push
            Map&amp;lt;String, Object&amp;gt; aps = new Map&amp;lt;String, Object&amp;gt;{
                'content-available' =&amp;gt; 1
            };

            // Full payload: aps + custom data
            Map&amp;lt;String, Object&amp;gt; payload = new Map&amp;lt;String, Object&amp;gt;();
            payload.put('aps', aps);
            payload.put('accountId', acc.Id);
            payload.put('accountName', acc.Name);
            payload.put('changedFields', changedFields);
            payload.put('type', 'ACCOUNT_UPDATE');

            Messaging.PushNotification msg = new Messaging.PushNotification();
            msg.setPayload(payload);

            Set&amp;lt;String&amp;gt; users = new Set&amp;lt;String&amp;gt;{ TARGET_USER_ID };

            try {
                msg.send('LSC_Real_time_Sync', users);
                System.debug('Silent push notification sent for Account: ' + acc.Name);
            } catch (Exception e) {
                System.debug(LoggingLevel.ERROR,
                    'Silent push notification failed for Account ' + acc.Name +
                    ': ' + e.getMessage());
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can apply the same pattern to Interactions or LMR-related objects, and further restrict to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LastModifiedById == current user&lt;/code&gt; to avoid system-generated updates.&lt;/li&gt;
&lt;li&gt;Excluding updates from sync queues or integration users.
This keeps pushing focused on &lt;strong&gt;user-initiated&lt;/strong&gt; changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Handle Push Notifications in the iOS App
&lt;/h2&gt;

&lt;p&gt;On the client side, you already have a unified handler to inspect incoming notification data.&lt;br&gt;
At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Foreground&lt;/strong&gt;: &lt;code&gt;userNotificationCenter(_:willPresent:withCompletionHandler:)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tap&lt;/strong&gt;: &lt;code&gt;userNotificationCenter(_:didReceive:withCompletionHandler:)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background/silent&lt;/strong&gt;: &lt;code&gt;application(_:didReceiveRemoteNotification:fetchCompletionHandler:)&lt;/code&gt;.
A simplified version that logs and reacts to custom keys:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any],
    fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -&amp;gt; Void
) {
    handlePushNotification(userInfo: userInfo)
    completionHandler(.newData)
}

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -&amp;gt; Void
) {
    let userInfo = notification.request.content.userInfo
    handlePushNotification(userInfo: userInfo)
    completionHandler([]) // no banner/sound for silent sync
}

private func handlePushNotification(userInfo: [AnyHashable: Any]) {
    // Example: respond to account/interaction updates
    if let type = userInfo["type"] as? String {
        switch type {
        case "ACCOUNT_UPDATE":
            if let accountId = userInfo["accountId"] as? String {
                // Trigger a targeted fetch for this account
                refreshAccount(withId: accountId)
            }
        case "INTERACTION":
            if let ids = userInfo["recordIds"] as? [String] {
                refreshInteractions(withIds: ids)
            }
        default:
            break
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;refreshAccount / refreshInteractions&lt;/code&gt; functions can use SFRestAPI to call your existing, territory-aware sync endpoints and update your local database. The important part is that:&lt;br&gt;
Push only carries &lt;strong&gt;IDs and type&lt;/strong&gt;, not full record data.&lt;br&gt;
The app decides how and when to fetch the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Considerations and Trade-Offs
&lt;/h2&gt;

&lt;p&gt;The approach led to some clear conclusions about how to use this pattern safely:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use push as a sideband hint, not as the primary sync mechanism&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Push is best-effort and subject to APNs/iOS behavior.&lt;/li&gt;
&lt;li&gt;Regular sync remains the authoritative way to catch all changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Limit scope to user-initiated changes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate pushes when the current user modifies records via Agentforce/LWC/web UI.&lt;/li&gt;
&lt;li&gt;Avoid pushing for backend/sync-queue changes to prevent noise and overload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Keep payload small and focused&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use push to send only &lt;code&gt;type&lt;/code&gt; and a small set of &lt;code&gt;recordIds&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Let the app fetch full records via REST/Composite or your existing sync APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Accept possible duplicate downloads&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A record refreshed via push may come down again in the next regular sync because its &lt;code&gt;LastModifiedDate&lt;/code&gt; is newer than the last sync date.&lt;/li&gt;
&lt;li&gt;With idempotent merge logic, this is a performance cost, not a correctness problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Prefer silent notifications for data refresh&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visible notifications are for user awareness; silent notifications are for data state.&lt;/li&gt;
&lt;li&gt;For offline-first apps, silent push is usually the right default for “real-time updates”.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Salesforce + Mobile SDK + APNs give you a way to &lt;strong&gt;target specific users&lt;/strong&gt; with push notifications based on business events.&lt;/li&gt;
&lt;li&gt;For real-time data freshness in offline-first apps, &lt;strong&gt;silent push notifications&lt;/strong&gt; are the right primitive: they wake the app, carry record IDs, and let the client fetch updated data.&lt;/li&gt;
&lt;li&gt;The full path:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;-- Connected App with APNs credentials.&lt;br&gt;
-- Device registration via &lt;code&gt;SFPushNotificationManager&lt;/code&gt;.&lt;br&gt;
-- Apex push using &lt;code&gt;Messaging.PushNotification&lt;/code&gt; to a user.&lt;br&gt;
-- iOS handlers logging and handling payloads, ready to trigger selective sync.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;By scoping push to user-initiated changes and treating it as a sideband channel, you get near real-time updates for critical objects without modifying the core sync algorithm or compromising data integrity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Salesforce&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Salesforce Mobile Notifications Implementation Guide (PDF) &lt;a href="https://resources.docs.salesforce.com/latest/latest/en-us/sfdc/pdf/salesforce_mobile_push_notifications_implementation.pdf" rel="noopener noreferrer"&gt;https://resources.docs.salesforce.com/latest/latest/en-us/sfdc/pdf/salesforce_mobile_push_notifications_implementation.pdf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Mobile Notifications Overview (HTML) &lt;a href="https://developer.salesforce.com/docs/atlas.en-us.pushImplGuide.meta/pushImplGuide/pns_overview.htm" rel="noopener noreferrer"&gt;https://developer.salesforce.com/docs/atlas.en-us.pushImplGuide.meta/pushImplGuide/pns_overview.htm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Push Notifications and Mobile SDK &lt;a href="https://developer.salesforce.com/docs/platform/mobile-sdk/guide/push-intro.html" rel="noopener noreferrer"&gt;https://developer.salesforce.com/docs/platform/mobile-sdk/guide/push-intro.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Enable Push Notifications in a Salesforce Mobile SDK iOS App &lt;a href="https://developer.salesforce.com/docs/atlas.en-us.pushImplGuide.meta/pushImplGuide/pns_client_app_ios.htm" rel="noopener noreferrer"&gt;https://developer.salesforce.com/docs/atlas.en-us.pushImplGuide.meta/pushImplGuide/pns_client_app_ios.htm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Using Push Notifications in iOS (Mobile SDK) &lt;a href="https://developer.salesforce.com/docs/platform/mobile-sdk/guide/push-using-ios.html" rel="noopener noreferrer"&gt;https://developer.salesforce.com/docs/platform/mobile-sdk/guide/push-using-ios.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Messaging.PushNotification Class (Apex Reference) &lt;a href="https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_classes_push_notification.htm" rel="noopener noreferrer"&gt;https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_classes_push_notification.htm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Messaging.PushNotification Methods (send, etc.) &lt;a href="https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_Messaging_PushNotification_methods.htm" rel="noopener noreferrer"&gt;https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_Messaging_PushNotification_methods.htm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Using Apex Triggers to Send Push Notifications &lt;a href="https://developer.salesforce.com/docs/atlas.en-us.pushImplGuide.meta/pushImplGuide/pns_apex_trigger.htm" rel="noopener noreferrer"&gt;https://developer.salesforce.com/docs/atlas.en-us.pushImplGuide.meta/pushImplGuide/pns_apex_trigger.htm&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Apple&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating a Remote Notification (payload format and 4 KB limit) &lt;a href="https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Creating the Remote Notification Payload (archived guide) &lt;a href="https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html" rel="noopener noreferrer"&gt;https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Payload Key Reference (JSON size and keys) &lt;a href="https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html" rel="noopener noreferrer"&gt;https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Technical Note TN2265 – Troubleshooting Push Notifications &lt;a href="https://developer.apple.com/library/archive/technotes/tn2265/_index.html" rel="noopener noreferrer"&gt;https://developer.apple.com/library/archive/technotes/tn2265/_index.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sending Notification Requests to APNs &lt;a href="https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Troubleshooting Push Notifications (UserNotifications) &lt;a href="https://developer.apple.com/documentation/usernotifications/troubleshooting-push-notifications" rel="noopener noreferrer"&gt;https://developer.apple.com/documentation/usernotifications/troubleshooting-push-notifications&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The end result is a Salesforce mobile app that feels much more responsive: important user-driven changes show up quickly, while the existing offline sync remains the safety net that keeps everything consistent.&lt;/p&gt;

</description>
      <category>salesforce</category>
      <category>ios</category>
      <category>mobile</category>
      <category>crm</category>
    </item>
  </channel>
</rss>
