Skip to main content
Blog

How to Ground Agentforce with Docusign Navigator Agreement Data

Author Liam Coates
Liam CoatesPrincipal Solution Architect

Summary14 min read

To ground Agentforce with real agreement data, you sync Docusign Navigator records into structured Salesforce fields using Named Credentials, Apex callouts, and a headless Lightning Web Component (LWC). This post walks through each component, from the service class to the Prompt Template.


If you've tried using Salesforce Agentforce or Prompt Builder to reason about a customer's agreements, you've likely hit the same wall: the AI can't see the data. Agreement details live in Docusign Navigator, but Salesforce fields are empty. The LLM hallucinates or returns generic advice because it has no structured context to work with.

This is fundamentally a data grounding problem. Grounding gives the LLM clear, structured context so it produces reliable output instead of guessing. To ground Agentforce with agreement data, you need to get that data out of Navigator and into fields that Prompt Builder can reference as merge fields.

This guide builds that bridge: an Apex service that calls the Navigator API, a controller that parses the response into 10 structured Account fields, a headless LWC that keeps the data fresh, and a Prompt Template that turns it all into an AI-powered health check. Each component is independent and reusable.

Prerequisites for the Navigator integration

This guide assumes you already have Named Credentials configured for the Navigator API using JWT authentication. If you haven’t done that yet, start with How to set up Salesforce authentication with JWT to access any Docusign API. You’ll also need access to the Navigator API, which is currently in limited availability.

System architecture for agreement health checks

The system has five components, each with a single responsibility. This separation matters for testability—you can unit-test the service class independently of the parsing logic—and for reuse, since the service class works for any Navigator endpoint, not just agreements.

1. NavigatorService: A reusable Apex class that handles all HTTP callouts to the Navigator API via Named Credentials. This is the secure communication layer.

2. NavigatorRefreshController: An @AuraEnabled controller that calls the service, parses the JSON response, and writes structured data into custom fields on the Account. This is the sync engine.

3. navigatorAutoRefresh LWC: A headless Lightning Web Component placed on the Account record page that checks whether data is stale and triggers a refresh only when needed. This is the automation layer.

4. NavigatorHealthCheck: An @InvocableMethod class that Agentforce can call to fetch and store Navigator data. This is the AI bridge.

5. Account Agreement Health Check Prompt Template: A Flex prompt template that reads the structured Account fields and generates an AI-powered health check. This is the AI instruction set.

Step 1: Create the Navigator service class

This class handles all communication with the Navigator API. It uses Salesforce Named Credentials, so you never manage tokens or authentication logic in your code — the callout:Navigator prefix tells Salesforce to handle the JWT handshake and Bearer token injection automatically.

Create the following file:

force-app/main/default/classes/NavigatorService.cls

// Apex (Salesforce)

public with sharing class NavigatorService {

    private static final String ACCOUNT_ID = 'YOUR_DOCUSIGN_ACCOUNT_ID';
    private static final String NAMED_CREDENTIAL = 'Navigator';

    public static String getAgreements(Map<String, String> filters, Integer limitCount) {
        String endpoint = buildEndpoint(filters, limitCount);

        HttpRequest req = new HttpRequest();
        req.setEndpoint(endpoint);
        req.setMethod('GET');
        req.setHeader('Content-Type', 'application/json');
        req.setTimeout(30000);

        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() == 200) {
            return res.getBody();
        } else {
            throw new NavigatorException(
                'Navigator API returned ' + res.getStatusCode() + ': ' + res.getBody()
            );
        }
    }

    public static String getAgreementById(String agreementId) {
        String endpoint = 'callout:' + NAMED_CREDENTIAL +
            '/accounts/' + ACCOUNT_ID +
            '/agreements/' + EncodingUtil.urlEncode(agreementId, 'UTF-8');

        HttpRequest req = new HttpRequest();
        req.setEndpoint(endpoint);
        req.setMethod('GET');
        req.setHeader('Content-Type', 'application/json');
        req.setTimeout(30000);

        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() == 200) {
            return res.getBody();
        } else {
            throw new NavigatorException(
                'Navigator API returned ' + res.getStatusCode() + ': ' + res.getBody()
            );
        }
    }

    private static String buildEndpoint(Map<String, String> filters, Integer limitCount) {
        String endpoint = 'callout:' + NAMED_CREDENTIAL +
            '/accounts/' + ACCOUNT_ID + '/agreements';

        List<String> queryParams = new List<String>();

        if (filters != null) {
            for (String key : filters.keySet()) {
                queryParams.add(
                    EncodingUtil.urlEncode(key, 'UTF-8') + '=' +
                    EncodingUtil.urlEncode(filters.get(key), 'UTF-8')
                );
            }
        }

        if (limitCount != null && limitCount > 0) {
            queryParams.add('limit=' + limitCount);
        }

        if (!queryParams.isEmpty()) {
            endpoint += '?' + String.join(queryParams, '&');
        }

        return endpoint;
    }

    public class NavigatorException extends Exception {}
}

Key points

  • Replace YOUR_DOCUSIGN_ACCOUNT_ID with your Docusign account GUID (found on the Apps and Keys page in Docusign Admin).

  • The NAMED_CREDENTIAL value must match the name of the Named Credential you created during the authentication setup.

  • The Navigator API endpoint follows the pattern: GET /v1/accounts/{accountId}/agreements. The base URL is https://api-d.docusign.com for the developer environment and https://api.docusign.com for production. Named Credentials handle this mapping for you.

  • The required OAuth scope is adm_store_unified_repo_read. Make sure this scope is included when configuring your Auth Provider.

A limit parameter controls how many agreements are returned per page. Use the ctoken (continuation token) parameter for pagination through larger result sets.

Note on filtering: The Navigator API supports two filtering approaches. For simple filters, use query parameters like parties.name_in_agreement=Acme+Corp. For complex queries, the API also supports OData $filter expressions with comparison operators (eq, ne, gt, ge, lt, le), logical operators (and, or), and the in operator. When using $filter with nested properties, use forward slash notation (e.g., provisions/effective_date) rather than dot notation. See the API reference for the full list of supported parameters.

Create the corresponding metadata file:

NavigatorService.cls-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>62.0</apiVersion>
    <status>Active</status>
</ApexClass>

Step 2: Create custom fields on Account

Rather than storing one massive JSON blob, we parse the Navigator response into structured fields. This is a deliberate design choice for AI grounding — by flattening the complex response into discrete fields, Prompt Builder can reference them as merge fields. The LLM receives well-structured input like “Active NDAs: 3, Pending Reviews: 2” instead of having to parse raw JSON, which produces more reliable, grounded output.

The structured fields also improve developer experience: they’re queryable in SOQL, visible on the page layout, and usable in reports and list views without any additional parsing.

Create the following field metadata files under force-app/main/default/objects/Account/fields/

API Name

Type

Purpose

Navigator_Total_Agreements__c

Number(5,0)

Total agreement count

Navigator_Active_NDAs__c

Number(5,0)

Count of non-expired NDAs

Navigator_Active_MSAs__c

Number(5,0)

Count of non-expired MSAs / Services Agreements

Navigator_Pending_Reviews__c

Number(5,0)

Agreements with review_status = PENDING

Navigator_Earliest_Expiration__c

Date

Soonest future expiration date

Navigator_Last_Synced__c

DateTime

When the data was last refreshed

Navigator_Expired_Agreements__c

Long Text(10000)

List of expired agreement names and dates

Navigator_Expiring_Soon__c

Long Text(10000)

Agreements expiring within 30 days

Navigator_Agreement_Summary__c

Long Text(32000)

One-line-per-agreement summary

Navigator_Agreements__c

Long Text(131072)

Raw JSON backup (hidden from layout)

Example field metadata for a Number field:

<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
    <fullName>Navigator_Total_Agreements__c</fullName>
    <label>Navigator: Total Agreements</label>
    <description>Total number of agreements found in DocuSign Navigator for this account.</description>
    <type>Number</type>
    <precision>5</precision>
    <scale>0</scale>
    <required>false</required>
    <defaultValue>0</defaultValue>
</CustomField>

Example field metadata for a Long Text Area field:

<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
    <fullName>Navigator_Agreement_Summary__c</fullName>
    <label>Navigator: Agreement Summary</label>
    <description>Condensed summary of all agreements from DocuSign Navigator.</description>
    <type>LongTextArea</type>
    <length>32000</length>
    <visibleLines>5</visibleLines>
</CustomField>

Field-level security

Grant read/write access to the System Administrator profile (or your target profile) by deploying a profile metadata file:

<?xml version="1.0" encoding="UTF-8"?>
<Profile xmlns="http://soap.sforce.com/2006/04/metadata">
    <fieldPermissions>
        <editable>true</editable>
        <field>Account.Navigator_Total_Agreements__c</field>
        <readable>true</readable>
    </fieldPermissions>
    <!-- Repeat for each Navigator field -->
</Profile>

Deploy the fields:

sf project deploy start \
  --source-dir force-app/main/default/objects/Account/fields \
  force-app/main/default/profiles \
  --target-org YourOrg --wait 5

Page layout tip: Add the numeric and date fields to a “DocuSign Navigator Data” section on the Account layout for at-a-glance visibility. The Long Text Area fields (summary, expired list, raw JSON) can be left off the layout — they’re still accessible to Apex and Prompt Templates even when not visible on the page.

Step 3: Create the refresh controller

This class is the bridge between the raw API response and your structured fields. It calls NavigatorService, parses the JSON response, and updates all 10 custom fields in a single DML operation.

force-app/main/default/classes/NavigatorRefreshController.cls

// Apex (Salesforce)

public with sharing class NavigatorRefreshController {

    private static final Date TODAY_DATE = Date.today();
    private static final Date THIRTY_DAYS = TODAY_DATE.addDays(30);

    @AuraEnabled
    public static void refreshNavigatorData(Id accountId) {
        Account acc = [SELECT Id, Name FROM Account WHERE Id = :accountId LIMIT 1];

        Map<String, String> filters = new Map<String, String>();
        filters.put('parties.name_in_agreement', acc.Name);

        String jsonBody = NavigatorService.getAgreements(filters, 50);
        Map<String, Object> parsed = (Map<String, Object>) JSON.deserializeUntyped(jsonBody);
        List<Object> agreements = (List<Object>) parsed.get('data');

        Integer totalAgreements = agreements.size();
        Integer activeNDAs = 0;
        Integer activeMSAs = 0;
        Integer pendingReviews = 0;
        Date earliestExpiration = null;
        List<String> expiredList = new List<String>();
        List<String> expiringSoonList = new List<String>();
        List<String> summaryLines = new List<String>();

        for (Object ag : agreements) {
            Map<String, Object> agreement = (Map<String, Object>) ag;
            String title = (String) agreement.get('title');
            String agType = (String) agreement.get('type');
            String status = (String) agreement.get('status');
            String reviewStatus = (String) agreement.get('review_status');

            Map<String, Object> provisions = (Map<String, Object>) agreement.get('provisions');
            Date expirationDate = parseDate(provisions, 'expiration_date');
            Date effectiveDate = parseDate(provisions, 'effective_date');

            List<Object> parties = (List<Object>) agreement.get('parties');
            String partyNames = getPartyNames(parties);

            Boolean isNda = agType != null && agType.containsIgnoreCase('Nda');
            Boolean isMsa = agType != null &&
                (agType.containsIgnoreCase('Msa') || agType.containsIgnoreCase('ServicesAgreement'));
            Boolean isExpired = expirationDate != null && expirationDate < TODAY_DATE;
            Boolean isExpiringSoon = expirationDate != null &&
                expirationDate >= TODAY_DATE && expirationDate <= THIRTY_DAYS;

            if (isNda && !isExpired) activeNDAs++;
            if (isMsa && !isExpired) activeMSAs++;
            if ('PENDING'.equalsIgnoreCase(reviewStatus)) pendingReviews++;

            if (isExpired) {
                expiredList.add(title + ' (' + agType + ', expired ' + String.valueOf(expirationDate) + ')');
            }

            if (isExpiringSoon) {
                expiringSoonList.add(title + ' (' + agType + ', expires ' + String.valueOf(expirationDate) + ')');
            }

            if (expirationDate != null && (earliestExpiration == null || expirationDate < earliestExpiration)) {
                if (expirationDate >= TODAY_DATE) {
                    earliestExpiration = expirationDate;
                }
            }

            String summaryLine = title + ' | ' + agType + ' | ' + status;
            if (partyNames != null) summaryLine += ' | Parties: ' + partyNames;
            if (effectiveDate != null) summaryLine += ' | Effective: ' + String.valueOf(effectiveDate);
            if (expirationDate != null) summaryLine += ' | Expires: ' + String.valueOf(expirationDate);
            summaryLines.add(summaryLine);
        }

        acc.put('Navigator_Agreements__c', jsonBody);
        acc.put('Navigator_Total_Agreements__c', totalAgreements);
        acc.put('Navigator_Active_NDAs__c', activeNDAs);
        acc.put('Navigator_Active_MSAs__c', activeMSAs);
        acc.put('Navigator_Pending_Reviews__c', pendingReviews);
        acc.put('Navigator_Earliest_Expiration__c', earliestExpiration);
        acc.put('Navigator_Last_Synced__c', Datetime.now());
        acc.put('Navigator_Expired_Agreements__c',
            expiredList.isEmpty() ? 'None' : String.join(expiredList, '\n'));
        acc.put('Navigator_Expiring_Soon__c',
            expiringSoonList.isEmpty() ? 'None' : String.join(expiringSoonList, '\n'));
        acc.put('Navigator_Agreement_Summary__c', String.join(summaryLines, '\n'));

        update acc;
    }

    private static Date parseDate(Map<String, Object> provisions, String fieldName) {
        if (provisions == null || !provisions.containsKey(fieldName)) return null;
        String dateStr = (String) provisions.get(fieldName);
        if (String.isBlank(dateStr)) return null;
        if (dateStr.contains('T')) dateStr = dateStr.substringBefore('T');
        return Date.valueOf(dateStr);
    }

    private static String getPartyNames(List<Object> parties) {
        if (parties == null || parties.isEmpty()) return null;
        List<String> names = new List<String>();
        for (Object p : parties) {
            Map<String, Object> party = (Map<String, Object>) p;
            String name = (String) party.get('name_in_agreement');
            if (name != null) names.add(name);
        }
        return String.join(names, ', ');
    }
}

What the parsing logic does

  • Iterates over each agreement in the data array from the Navigator API response.

  • Classifies agreements by type — NDAs (Nda) and MSAs (Msa or ServicesAgreement) — and counts active (non-expired) ones.

  • Checks review_status for pending reviews.

  • Compares provisions.expiration_date against today’s date to flag expired and soon-to-expire agreements.

  • Tracks the earliest future expiration date across all agreements.

  • Builds a human-readable one-line summary per agreement for the summary field.

Name matching constraint: This implementation filters by the exact Salesforce Account Name using parties.name_in_agreement. In production, the party name in Navigator may not always match the Salesforce Account Name exactly (e.g., “Acme Corp” vs “Acme Corporation”). Consider storing a Docusign-specific identifier on the Account record, or using the Navigator API’s OData $filter with or operators to match multiple name variations.

Navigator API Response Structure Reference

{
  "data": [
    {
      "id": "abc-123",
      "title": "Professional Services Agreement",
      "type": "ServicesAgreement",
      "status": "COMPLETE",
      "category": "BusinessServices",
      "review_status": "PENDING",
      "parties": [
        { "name_in_agreement": "Acme Corp" },
        { "name_in_agreement": "Globex Inc" }
      ],
      "provisions": {
        "effective_date": "2025-01-15",
        "expiration_date": "2026-01-15",
        "renewal_term": "12 months",
        "auto_renewal": true
      }
    }
  ],
  "pagination": {
    "next_cursor": "eyJsYXN0X2lkIjoiYWJjLTEyMyJ9"
  }
}

Step 4: Create the auto-refresh Lightning Web Component

To keep Navigator data fresh without manual intervention, we create a headless LWC that sits on the Account record page. It has no visible UI — it just checks whether the data is stale and triggers a refresh if needed.

The “check-before-sync” pattern here is important for system reliability. Rather than firing a callout on every page load, the LWC reads the Navigator_Last_Synced__c timestamp and only calls the API if the data is older than one hour. This prevents excessive API calls in high-traffic orgs where many users view the same Account throughout the day.

force-app/main/default/lwc/navigatorAutoRefresh/navigatorAutoRefresh.js

import { LightningElement, api, wire } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import refreshNavigatorData from '@salesforce/apex/NavigatorRefreshController.refreshNavigatorData';

const FIELDS = ['Account.Navigator_Last_Synced__c'];
const REFRESH_INTERVAL_MS = 3600000; // 1 hour

export default class NavigatorAutoRefresh extends LightningElement {
    @api recordId;
    _hasRefreshed = false;

    @wire(getRecord, { recordId: '$recordId', fields: FIELDS })
    handleRecord({ data }) {
        if (data && !this._hasRefreshed) {
            const lastSynced = data.fields.Navigator_Last_Synced__c.value;
            const now = Date.now();
            const lastSyncedTime = lastSynced ? new Date(lastSynced).getTime() : 0;

            if (now - lastSyncedTime > REFRESH_INTERVAL_MS) {
                this._hasRefreshed = true;
                refreshNavigatorData({ accountId: this.recordId })
                    .then(() => {
                        console.log('Navigator data refreshed for account: ' + this.recordId);
                    })
                    .catch(error => {
                        console.error('Navigator refresh failed:', error);
                    });
            }
        }
    }
}

force-app/main/default/lwc/navigatorAutoRefresh/navigatorAutoRefresh.html

<template></template>

force-app/main/default/lwc/navigatorAutoRefresh/navigatorAutoRefresh.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>62.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <objects>
                <object>Account</object>
            </objects>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Rate limits: Docusign APIs enforce per-account hourly rate limits (returned in the X-RateLimit-Limit response header). The one-hour interval in REFRESH_INTERVAL_MS is a conservative default — adjust it based on your org's traffic and API allocation. In orgs with hundreds of users, you may want to increase this to 4–8 hours or move to a scheduled batch job instead.

Adding the component to the account page

  1. Navigate to any Account record in Salesforce.

  2. Click the gear icon and select Edit Page to open Lightning App Builder.

  3. Find Navigator Auto Refresh in the component panel on the left.

  4. Drag it anywhere on the page (it renders nothing visible).

  5. Save and activate the page.

Step 5: Create the Agentforce invocable action

To make this data actionable for AI, you need an @InvocableMethod class that Agentforce can call. This class receives the Account record as input, calls the Navigator API, and stores the raw JSON on the Account for the Prompt Template to use.

CapabilityType update: If you’ve seen older examples that use CapabilityType='FlexTemplate://...' on the @InvocableMethod annotation, note that Salesforce has retired the Flex CapabilityType. You should no longer include a CapabilityType attribute for Flex templates. Instead, deploy the Apex class without it and add it as a resource directly in Prompt Builder (see Step 6). This also eliminates the circular deployment dependency that older guides warned about.

force-app/main/default/classes/NavigatorHealthCheck.cls

// Apex (Salesforce)

public with sharing class NavigatorHealthCheck {

    @InvocableMethod(
        label='Account Agreement Health Check'
        description='Retrieves all DocuSign Navigator agreements for an Account, stores the data on the record, and returns it for health check analysis.'
        callout=true
    )
    public static List<Output> getHealthCheckData(List<Input> inputs) {
        Input input = inputs[0];

        String accountName = '';
        Id accountId;
        if (input.RelatedEntity != null) {
            accountName = input.RelatedEntity.Name;
            accountId = input.RelatedEntity.Id;
        }

        Output output = new Output();
        try {
            Map<String, String> filters = new Map<String, String>();
            if (String.isNotBlank(accountName)) {
                filters.put('parties.name_in_agreement', accountName);
            }

            String jsonBody = NavigatorService.getAgreements(filters, 50);

            if (accountId != null) {
                Account acc = new Account(Id = accountId, Navigator_Agreements__c = jsonBody);
                update acc;
            }

            output.Prompt = '';
        } catch (Exception e) {
            if (accountId != null) {
                Account acc = new Account(
                    Id = accountId,
                    Navigator_Agreements__c = '{"error": "' + e.getMessage().escapeJava() + '"}'
                );
                update acc;
            }
            output.Prompt = '';
        }

        return new List<Output>{ output };
    }

    public class Input {
        @InvocableVariable(required=true)
        public Account RelatedEntity;
    }

    public class Output {
        @InvocableVariable(label='Prompt' description='The generated health check response.')
        public String Prompt;
    }
}

Requirements for the invocable method

  • The input class must have a field named RelatedEntity of the sObject type matching the template’s related entity (here, Account).

  • The output class must have a field named Prompt of type String. This is the required output variable for Prompt Builder integration.

  • The callout=true attribute is required because the method makes HTTP callouts.

  • No CapabilityType attribute is needed. After deploying, you’ll add this class as a resource in Prompt Builder directly.

Step 6: Create the Prompt Template

The prompt template is a Flex type (einstein_gpt__flex) template that reads the structured Account fields populated by the refresh controller. Because the data is already parsed into clean fields, the LLM receives well-structured input and produces reliable, grounded output.

force-app/main/default/genAiPromptTemplates/Account_Agreement_Health_Check.genAiPromptTemplate-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<GenAiPromptTemplate xmlns="http://soap.sforce.com/2006/04/metadata">
    <activeVersion>2</activeVersion>
    <description>Performs an agreement health check for an Account using structured
        DocuSign Navigator data synced to Account fields.</description>
    <masterLabel>Account Agreement Health Check</masterLabel>
    <relatedEntity>Account</relatedEntity>
    <templateVersions>
        <content>You are an agreement health check analyst for: {!$Input:Account.Name}.

Use ONLY the DocuSign Navigator data below. Do NOT make up any information.

--- DocuSign Navigator Data (Last Synced: {!$Input:Account.Navigator_Last_Synced__c}) ---

Total Agreements: {!$Input:Account.Navigator_Total_Agreements__c}
Active NDAs: {!$Input:Account.Navigator_Active_NDAs__c}
Active MSAs/Services Agreements: {!$Input:Account.Navigator_Active_MSAs__c}
Pending Reviews: {!$Input:Account.Navigator_Pending_Reviews__c}
Earliest Upcoming Expiration: {!$Input:Account.Navigator_Earliest_Expiration__c}

Expired Agreements:
{!$Input:Account.Navigator_Expired_Agreements__c}

Expiring Within 30 Days:
{!$Input:Account.Navigator_Expiring_Soon__c}

Agreement Details:
{!$Input:Account.Navigator_Agreement_Summary__c}

--- End of Navigator Data ---

Provide a structured health check:

**Account Agreement Overview**
Summarize the account's agreement portfolio using the data above.

**Flags and Alerts**
Based ONLY on the data provided:
1. If there are expired agreements listed above, flag each as URGENT and name them.
2. If there are agreements expiring within 30 days listed above, flag each as WARNING and name them.
3. If pending reviews is greater than 0, flag as INFO.
If none, say No issues found.

**Recommended Actions**
For each flag, recommend a specific action:
- Expired NDAs: Send a new NDA to the relevant party
- Expiring MSAs: Create a renewal opportunity
- Pending reviews: Review and approve the agreement

If no flags, state the account agreements are healthy.

Be concise, professional, and action-oriented.</content>
        <inputs>
            <apiName>RelatedEntity</apiName>
            <definition>SOBJECT://Account</definition>
            <referenceName>Input:Account</referenceName>
            <required>true</required>
        </inputs>
        <primaryModel>sfdc_ai__DefaultOpenAIGPT4</primaryModel>
        <status>Published</status>
        <versionNumber>2</versionNumber>
    </templateVersions>
    <type>einstein_gpt__flex</type>
    <visibility>Global</visibility>
</GenAiPromptTemplate>

Template metadata notes

  • type must be einstein_gpt__flex (not just Flex).

  • The relatedEntity is Account, which means the template appears in Prompt Builder when you select Account context.

  • Merge fields use the syntax {!$Input:Account.FieldName} to pull live values from the Account record. This is the grounding mechanism — the LLM sees real data, not hallucinated content.

  • The primaryModel is set to sfdc_ai__DefaultOpenAIGPT4 but can be changed to any model available in your org.

  • apiVersion must be 62.0 or higher — GenAiPromptTemplate metadata is not available in earlier API versions.

Adding the Apex resource

After deploying both the Apex class and the template, open Prompt Builder from Setup, find the Account Agreement Health Check template, click the Resource field in the Prompt section, and select Apex. Choose the NavigatorHealthCheck class. This binds the invocable method to the template without needing a CapabilityType annotation.

Deploy:

sf project deploy start \
  --source-dir force-app/main/default/genAiPromptTemplates \
  --target-org YourOrg --api-version 62.0 --wait 5

Step 7: Deploy and test

Full deployment

Because we no longer use CapabilityType, the deployment order is simpler — there's no circular dependency to work around:

# 1. Deploy fields and FLS
sf project deploy start \
  --source-dir force-app/main/default/objects/Account/fields \
  force-app/main/default/profiles \
  --target-org YourOrg --api-version 62.0 --wait 5

# 2. Deploy all Apex classes
sf project deploy start \
  --source-dir force-app/main/default/classes \
  --target-org YourOrg --api-version 62.0 --wait 5

# 3. Deploy the Prompt Template
sf project deploy start \
  --source-dir force-app/main/default/genAiPromptTemplates \
  --target-org YourOrg --api-version 62.0 --wait 5

# 4. Deploy the LWC
sf project deploy start \
  --source-dir force-app/main/default/lwc \
  --target-org YourOrg --api-version 62.0 --wait 5

# 5. In Prompt Builder, add NavigatorHealthCheck as an Apex resource
#    to the Account Agreement Health Check template.

Quick test with Anonymous Apex

Run this in the Developer Console or via the CLI to verify the integration end-to-end:

// Replace with a real Account ID from your org
Id accId = '001XXXXXXXXXXXX';
NavigatorRefreshController.refreshNavigatorData(accId);

Account acc = [SELECT
    Navigator_Total_Agreements__c,
    Navigator_Active_NDAs__c,
    Navigator_Active_MSAs__c,
    Navigator_Pending_Reviews__c,
    Navigator_Earliest_Expiration__c,
    Navigator_Last_Synced__c,
    Navigator_Expired_Agreements__c,
    Navigator_Expiring_Soon__c
FROM Account WHERE Id = :accId];

System.debug('Total Agreements: ' + acc.Navigator_Total_Agreements__c);
System.debug('Active NDAs: ' + acc.Navigator_Active_NDAs__c);
System.debug('Active MSAs: ' + acc.Navigator_Active_MSAs__c);
System.debug('Pending Reviews: ' + acc.Navigator_Pending_Reviews__c);
System.debug('Earliest Expiration: ' + acc.Navigator_Earliest_Expiration__c);
System.debug('Last Synced: ' + acc.Navigator_Last_Synced__c);
System.debug('Expired: ' + acc.Navigator_Expired_Agreements__c);
System.debug('Expiring Soon: ' + acc.Navigator_Expiring_Soon__c);

Run via CLI:

sf apex run --file scripts/apex/hello.apex --target-org YourOrg

You should see output like:

Total Agreements: 4
Active NDAs: 0
Active MSAs: 3
Pending Reviews: 4
Earliest Expiration: 2026-03-03 00:00:00
Last Synced: 2026-02-25 11:51:45
Expired: Professional Services Agreement — Atlas Power Modules (ServicesAgreement, expired 2025-11-07)
Expiring Soon: Contract for Information Products 2026-1 (ServicesAgreement, expires 2026-03-03)

Testing the prompt template

  1. Navigate to an Account record in Salesforce (the LWC will auto-refresh the data if stale)

  2. Open Prompt Builder from Setup

  3. Find Account Agreement Health Check and preview it with your Account selected

  4. The generated response should reference only real agreements from Navigator, with accurate flags for expired and expiring agreements

Constraints and tradeoffs

Every integration has operational limits that affect scalability and performance. Understanding these constraints helps you design a system that remains reliable even as your agreement volume or user count grows.

API rate limits. Every callout counts against your Docusign account’s hourly rate limit. The LWC in this guide includes a timestamp check to only refresh when data is older than one hour, but you should tune this interval based on your org’s traffic and API allocation. For high-volume orgs, consider moving to a scheduled batch job that syncs all Accounts overnight.

Name matching. This implementation matches agreements by exact Salesforce Account Name. In production, party names in Navigator may differ (e.g., “Acme Corp” vs “Acme Corporation”). Consider storing a Docusign-specific party identifier on the Account record, or using the Navigator API’s OData $filter with or operators to match multiple name variations.

Field limits. Long Text Area fields have maximum lengths (32,000 characters for the summary, 131,072 for raw JSON). For accounts with very large agreement portfolios, you may need to truncate or paginate the data.

Limited availability. The Navigator API is currently in limited availability. Check the Navigator API documentation for the latest on access and GA timelines.

CapabilityType retirement. Salesforce has retired the FlexTemplate:// CapabilityType binding. If you’re following an older guide that uses it, remove the attribute and add the Apex class as a resource in Prompt Builder instead.

Next steps

You now have a working data pipeline from Docusign Navigator into Salesforce, with AI-powered health checks on top. Here are some ways to extend it:

Add a scheduled batch job: Replace or supplement the LWC with a Schedulable Apex class that syncs all Accounts nightly, reducing reliance on page-load triggers.

Build a renewal workflow: Use the Navigator_Expiring_Soon__c field to trigger a Flow that creates renewal Opportunities automatically.

Extend to other objects: The same pattern works for syncing Navigator data to Opportunities, Contacts, or custom objects.

Add the getAgreement endpoint: NavigatorService.getAgreementById() is already in the service class. Build a detail view that shows full agreement provisions when a user clicks into a specific agreement.

FAQs

  1. What is data grounding in Agentforce? Grounding gives the LLM structured, real data so it reasons from facts rather than guessing. Without it, Agentforce can't see agreement details in external systems and will hallucinate.

  2. Why use Named Credentials instead of managing tokens in code? Named Credentials handle the full JWT handshake and token injection automatically. Your Apex never touches OAuth tokens directly — Salesforce manages it all via the callout prefix.

  3. Why store agreement data in structured fields instead of a JSON blob? Prompt Builder references fields as merge fields, so the LLM gets clean inputs like "Active NDAs: 3" instead of parsing raw JSON. Structured fields are also queryable in SOQL and usable in reports.

  4. How does the LWC prevent excessive Navigator API calls? It checks the Navigator_Last_Synced__c timestamp on page load and only calls the API if data is older than one hour. A _hasRefreshed flag prevents duplicate calls within the same session.

  5. What happened to the FlexTemplate CapabilityType in Salesforce? Salesforce retired it. Remove the CapabilityType attribute from your @InvocableMethod and add the Apex class as a resource directly in Prompt Builder instead.

  6. How does Prompt Builder reference custom Salesforce fields as merge fields? It uses {!$Input:Account.FieldName} syntax to pull live values from the related record at runtime. Any custom field on the related object is available as a merge field.

  7. What is the Navigator API's current availability status? It's in limited availability. You need to request access - check the Navigator API docs for the latest on GA timelines.

Additional resources

Author Liam Coates
Liam CoatesPrincipal Solution Architect

Liam Coates leads enterprise architecture initiatives at Docusign, helping organizations design and implement scalable solutions across the Docusign platform. Liam has been at Docusign for 11 years.

More posts from this author

Related posts

  • Developers

    How to Create an Azure Custom Extension App for Docusign IAM

    Author Mohamed Ali
    Mohamed Ali
    How to Create an Azure Custom Extension App for Docusign IAM

Docusign IAM is the agreement platform your business needs

Start for FreeExplore Docusign IAM
Person smiling while presenting