DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • The LLM Selection War Story: Part 3 - Decision Framework Through Failure Tolerance
  • Patterns for Building Production-Ready Multi-Agent Systems
  • Token Attribution Framework for Agentic AI in CI/CD
  • The Middleware Gap in AI Agent Frameworks

Trending

  • How to Submit a Post to DZone
  • Mastering Fluent Bit: Beginners' Guide for Contributing to Our CNCF Project Website
  • The Missing `bandit` for AI Agents: How I Built a Static Analyzer for Prompt Injection
  • Identity in Action
  1. DZone
  2. Data Engineering
  3. AI/ML
  4. Logging What AI Agents Do in Salesforce: A Simple One-Object Audit Framework

Logging What AI Agents Do in Salesforce: A Simple One-Object Audit Framework

Log every AI agent action to one custom object, and force the LLM to include a reasoning field in every tool call so you always know why it did what it did.

By 
Ronith Pingili user avatar
Ronith Pingili
·
Jun. 09, 26 · Analysis
Likes (0)
Comment
Save
Tweet
Share
67 Views

Join the DZone community and get the full member experience.

Join For Free

Picture a simple scenario. An AI agent is wired into your Case page in Salesforce. A customer sends a reply that sounds like the issue is resolved. The agent reads the conversation, decides the case can be closed, and updates the status to "Closed."

A week later, the customer calls in frustrated. "Why did you close my case? My issue wasn't resolved."

You go to the Case in Salesforce to investigate. The audit trail tells you almost nothing useful. CreatedBy says "Integration User." LastModifiedBy says the same. Field history tracking shows the status changed from "Open" to "Closed" at 3:37 pm last Tuesday. None of it tells you the one thing you actually need to know: why did the agent do that?

This is the gap that shows up the minute AI agents start taking real action inside Salesforce. The fix is small. One custom object, a handful of fields, and a habit of asking the agent to explain itself.

Where the Problem Begins

Standard Salesforce auditing was built for two kinds of actors: human users and deterministic automation. When a human closes a case, you can ask them why. When a Flow closes a case, the criteria are sitting right there in the metadata. When an Apex trigger closes a case, the logic is in the code.

AI agents are neither of those. The decision to close the case lived inside a language model's response, and the moment the action committed, that reasoning was gone. Unless you captured it.

The Idea

Drop a single custom object into the org. Every time the agent does something, it writes one row. That row captures three things:

  • What the agent did
  • What context it had when deciding
  • Why it made the call

That's the whole framework. One object, one row per action, three buckets of data.

The Core Object: Agent_Action_Log__c

Here are the fields that matter. Everything else is optional polish.

  • Triggering User  – Lookup to the User whose interaction caused the agent to run.
  • Related Case – Lookup to the Case the agent was working on.
  • Action Type – Picklist with values like Create Record, Update Record, Send Email, Call External API.
  • Tool Called – The specific Apex method, Flow, or API the agent invoked.
  • Inputs – The arguments the agent passed to the tool (long text).
  • Context Snapshot – The relevant context the agent had at decision time, such as record state and recent activity (long text).
  • Reasoning – The agent's stated rationale, captured verbatim from the LLM response (long text).
  • Confidence – A number between 0 and 1 if the agent reports one. Optional but useful for reporting.
  • Status – Success or  Failure.
  • Error – If anything went wrong, the error message lands here.

Eleven fields. One object. Keep it small until you need it to be bigger. Because Related Case is a lookup field, every Case page can show a related list of Agent Action Logs. 

How the Reasoning Actually Gets Captured

This is the part most teams get wrong, so it's worth slowing down on.

The LLM doesn't volunteer its reasoning. You have to ask for it as part of the response itself. The way you do that is by defining every tool the agent can call with a JSON schema, and adding a reasoning field to that schema right next to the actual arguments.

Back to the case-closing example. Normally the update_case_status tool would accept two arguments: case_id and new_status. With this framework, add a third required field:

JSON
 
{
  "type": "function",
  "function": {
    "name": "update_case_status",
    "description": "Updates the status of a Salesforce Case.",
    "parameters": {
      "type": "object",
      "properties": {
        "case_id": {
          "type": "string",
          "description": "The 18-character Salesforce Case Id."
        },
        "new_status": {
          "type": "string",
          "description": "The new status value to apply."
        },
        "reasoning": {
          "type": "string",
          "description": "Why this status change is appropriate given the context."
        }
      },
      "required": ["case_id", "new_status", "reasoning"]
    }
  }
}


Here's the important part. The LLM is not calling Salesforce. Your Apex code calls the LLM, the LLM sends back a JSON payload describing the tool call (function name plus arguments), and your Apex code is the one that actually executes the action. So when the response comes back with case_id, new_status, and reasoning, your code reads all three out of the JSON. The first two drive the update on the Case. The third gets written into the Reasoning field on the new Agent_Action_Log__c record. The agent's words land in your audit log verbatim.

If you turn on strict mode in your tool definition (OpenAI's strict: true), the model is forced to return every required field, including reasoning. That makes empty reasoning rare. Even so, defensive code should reject any tool call missing the field and ask the model to retry, in case strict enforcement isn't available on the path you're using.

The reasoning field is the difference between "the agent closed this case" and "the agent closed this case because the customer's last message said 'Thanks, that solved it, you can close this out.'" One of those is an audit log entry. The other is actually useful.

How It All Works

Walking through the case-closing scenario end to end:

  1. A customer reply lands on a Case, triggering the agent.
  2. The framework gathers context about the Case: the Triggering User, the Related Case, and a Context Snapshot showing the last few messages on the case.
  3. The agent calls update_case_status with case_id, new_status set to "Closed", and a reasoning string. The framework now has everything it needs: Action Type, Tool Called, Inputs, Reasoning, and Confidence.
  4. The framework executes the actual status update on the Case.
  5. The framework writes one Agent_Action_Log__c row capturing everything: the context, the reasoning, and the outcome (Status set to "Success", or Error if the update failed).
  6. If the agent takes a follow-up action (say, sending the customer a notification email), that's a new row on the same Case, with its own reasoning.

From any Case page, the related list shows every agent action that ever touched that record. From a Salesforce report, compliance can pull every "Close Case" action where Confidence was below 0.7 in the last quarter. From a debugger's view, you can find out exactly what the agent thought it was doing the moment it did it.

Why This Approach Works

  • One object, one place to look. No traversing relationships, no joining across logs.
  • The "why" lives next to the "what." Reasoning is captured at decision time, not reconstructed after the fact.

Final Thoughts

The case-closing example is intentionally small. But the same pattern handles every other agent action, whether that's sending an email, escalating to a specialist, updating a field, or posting a notification. Each one becomes another row on the same object, with the same fields, captured the same way.

That's the whole point. You don't need an elaborate observability stack to make AI agents auditable in Salesforce. You need one custom object, a habit of forcing the model to explain itself, and a wrapper around your tool calls that writes a row every time.

The next time someone asks why the agent did something, the answer is already sitting in a record on the page they're looking at.

AI Framework large language model

Opinions expressed by DZone contributors are their own.

Related

  • The LLM Selection War Story: Part 3 - Decision Framework Through Failure Tolerance
  • Patterns for Building Production-Ready Multi-Agent Systems
  • Token Attribution Framework for Agentic AI in CI/CD
  • The Middleware Gap in AI Agent Frameworks

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook