Vault Java SDK Overview

The Vault Java SDK is a powerful tool in the Vault Platform, allowing developers to extend Vault and deliver custom capabilities and experiences to Veeva customers. It provides a completely new experience in developing industry cloud applications, leveraging industry standard tools to develop and debug and integrating seamlessly with Vault in the cloud.

About the Java SDK Service Account

The 22R2 release added the Run Custom Code as Java SDK Service Account setting to Admin > Settings > General Settings. If the Run Custom Code as Java SDK Service Account setting is enabled in your Vault:

The Java SDK Service Account user appears in the Users & Groups Admin tab in all Vaults but does not affect license counts.

In 22R2, this setting was off by default, but Admins could enable it from the General Settings page.

Default behavior for this setting will change as follows in upcoming releases:

Extending Vault

Developers can use the Vault Java SDK to extend Vault by implementing custom code, such as triggers and actions.

alt text

Getting Started

Prerequisites

To develop with the Vault Java SDK, you need all of the tools for Java development, such as a Java Development Kit (JDK) and an Integrated Development Environment (IDE). You will also need a Vault to test your new Vault extensions before deploying them to production.

To get more help getting started, you can post or find an answer in the Developer forum.

  1. If you are unfamiliar with Vault, we suggest watching Vault Navigation Basics in Vault Help before you begin.
  2. You must be a Vault Owner to complete the Getting Started. Learn more about related permissions.
  3. To deploy code, you must enable configuration packages in your Vault. Check if this feature is enabled in Admin > Deployment. If your Vault has the Inbound and Outbound Packages pages here, this is enabled. If not, learn how to enable configuration packages in Vault Help.
  4. Download and install JDK 1.8.
  5. Download and install a Java IDE. We recommend Intellij IDEA Community Edition®, which is what we use for our tutorials.
  6. Request access to a Vault from your Vault Admin. They can provision a sandbox for use.
  7. Clone or download the sample Maven project vSDK Hello World from GitHubTM. Click the Code button and select Download ZIP to download the project.

Step 1: Vault Setup

First, you need to configure your Vault so the sample SDK trigger runs smoothly. The sample trigger runs on the vSDK Hello World object, which you must add to your Vault. You can do this by deploying a prepackaged set of components (.vpk) included in the sample project.

  1. Log in to your Vault and navigate to Admin > Deployment > Inbound Packages and click Import.
  2. Locate and select the \deploy-vpk\vsdk-helloworld-components\vsdk-helloworld-components.vpk file in your downloaded or cloned project folder. You will receive an email when Vault finishes importing the package. Refresh the page to find the imported package. You can open it to review the details.
  3. From the Actions menu, select Review & Deploy. Vault displays a list of all components in the package.
  4. Click Next.
  5. On the confirmation page, review and click Finish. You will receive an email when Vault completes the deployment.
  6. Check that the VPK imported successfully in Business Admin > Objects. If the vSDK Hello World object exists here, your Vault setup is complete.

Step 2: Development Setup

Now that you’ve set up your Vault, you can move on to setting up your development environment.

Import the Maven Project to IntelliJ®

  1. From IntelliJ®, select File > Open.
  2. Navigate to your downloaded or cloned project directory and locate the vsdk-helloworld-release folder and select the pom.xml file.
  3. Click Open.
  4. In the Open Project dialog, click Open as Project. If prompted, choose Trust Project. IntelliJ® imports the project and automatically downloads the Vault Java SDK dependencies.
  5. Verify that the Maven: com.veeva.vault.sdk.api:vault-sdk-api library is present in the External Libraries section. If this file is not present or the External Libraries section is empty, make sure you have access to the Internet and your browser can load repo.veevavault.com. You should also make sure your POM file is set up with the correct Vault SDK version.

IntelliJ® Setup

The Java SDK version in IntelliJ® must match the version you installed during the Prerequisites setup.

To set the Java SDK version in IntelliJ® to 1.8:

  1. Navigate to File > Project Structure > Project Settings > Project.
  2. In the SDK field, select the JDK 1.8 that you installed.
  3. Click Apply and then OK.

POM Setup

The Java SDK library version must match the Vault version. You can update the version by editing the <vault.sdk.version> attribute in your POM file.

  1. Verify the version of your Vault. You can find your Vault version in Admin > Settings > General Settings. You don’t need to worry about your Vault’s build number.
  2. In IntelliJ®, navigate to your pom.xml file.
  3. Update the <vault.sdk.version> to your Vault version, using only periods (.) and not the letter R. For example, a Vault on version 18R3.0 should look like this:

    <properties>
        <vault.sdk.version>[18.3.0-release0, 18.3.0-release1000000]</vault.sdk.version>
    </properties>
    
  4. If prompted, select Import Changes. You can also Enable Auto-Import to instruct Maven to automatically import any future changes. In newer versions of IntelliJ®, you may need to import changes manually by right-clicking the pom.xml file and selecting Maven > Reload project.

  5. In the External Libraries section of IntelliJ®, verify the Maven: com.veeva.vault.sdk.api:vault-sdk-api library shows your Vault version.

Debugger Setup

After downloading the Vault Java SDK artifacts in your Maven project, you can use the Vault Java SDK Debugger. You must have the standard Vault Owner security profile. Learn more in related permissions.

To set up the debugger:

  1. Navigate to Run > Edit Configurations….
  2. Click Add New Configuration and select Application from the drop-down.
  3. Give your configuration a Name.
  4. Add the following data to your new configuration:

Main Class: com.veeva.vault.sdk.debugger.SdkDebugger. The SDK Debugger Main Class should auto-complete as you begin to type. If it does not autocomplete, or if your IDE cannot recognize this Main Class, you may have completed a previous step incorrectly.

Program Arguments:

  1. Click Apply and then OK.
  2. Click Run to run the project and attach the debugger to your Vault.

If your connection to the debugger is successful, you will see a console message stating Welcome to the Vault Java SDK Debugger and additional information such as your user, host, and debugger version. Continue to Step 3: Run Code.

Troubleshooting

You may encounter the following errors when running the debugger:

Step 3: Run Code

The sample project downloaded for this guide contains a basic example of a trigger. When you run your project, all of the Vault Extension code in your project becomes live and running in your Vault.

Your sample code is a BEFORE_INSERT trigger on the vSDK Hello World object. This means the trigger executes right before the object record saves. The sample trigger will then show an error, defined on lines 23 and 24 of the sample code.

  1. Log in to your Vault.
  2. Navigate to Business Admin > Objects > vSDK Hello World.
  3. Click Create.
  4. Enter your name in the Name field.
  5. Click Save. You should see the following text:

That’s your trigger in action! The “Hello, World” code only runs while the debugger is running. Once the debugger is stopped, the trigger stops running and this popup will no longer appear.

Step 4: Debug Code

Instead of running the code, you can place breakpoints and debug the Vault extension class line by line. Let’s modify this Hello World class to say hello to a name you enter in a field.

  1. Click Stop in IntelliJ® to turn off the debugger.
  2. Open the HelloWorld Java file in IntelliJ®, which is in the javasdk folder.
  3. Comment out line 21 by adding two slashes to the beginning of the line:
  4. Uncomment out the lines 23 and 24 by removing the two slashes at the beginning of the line:
  5. Add breakpoints on the new lines uncommented out from Step 4 by clicking just to the right of the line number.
  6. Instead of Run, Debug your program.
  7. In Vault, Create a new vSDK Hello World record with your Name and click Save. You should see the code execution is transferred from the server to your code locally in IntelliJ®.
  8. To watch your code execute on each breakpoint, click the Resume Program button in the console sidebar.
  9. Back in your Vault, you should see a slightly different error message:
  10. When you are finished, click Stop to turn off the debugger.

Step 5: Deploy Code

Vault extensions stop running when you stop the debugger. To make your SDK code run automatically for all users, you must deploy them to your Vault. We do this with a VPK, the same way we deployed a VPK in the Vault Setup section of this guide.

  1. In IntelliJ®, open the vaultpackage.xml file.
  2. Replace firstname.lastname@example.com with your Vault user name.
  3. From your computer, select both the javasdk folder and the vaultpackage.xml file with CMD+click on MacOS®, or CTRL+click on Windows®. If you’re having trouble finding these files on your computer, you can right-click the filename in IntelliJ® and select Open In > Finder on MacOS®, or Open File Location on Windows®.
  4. Right-click your files and select Compress 2 Items on MacOS®, or Send to > Compressed (zipped) folder on Windows®. If you are on MacOS® and cannot right-click, you can CTRL+click these items.
  5. Rename your new .zip to .vpk.
  6. Back in Vault, navigate to Admin > Deployment > Inbound Packages and click Import.
  7. Find and select your newly created VPK file from your computer.
  8. From the Actions menu, select Review & Deploy.
  9. Click Next.
  10. On the confirmation page, review and click Finish.
  11. Navigate to Admin > Configuration > Record Triggers. If you see your Hello World trigger here, you’ve successfully deployed the extension to Vault!
  12. Back in Business Admin > Objects, navigate to the vSDK Hello World object and click Create.
  13. Name your trigger “Deployed Trigger” and click Save. You should see the following error message:

Congratulations, you’ve completed the Vault Java SDK Getting Started!

Developing Code

To develop code, you need to have a Maven project. You need to make sure your POM file is set up correctly, and your src folder is under the javasdk folder.

POM Setup

The artifacts (.jars) for the Vault Java SDK are distributed by a Maven Repository Manager. This allows you to easily download the Vault Java SDK and all its dependent libraries by simply setting up a Maven project pointing to the Maven Repo Manager in the pom.xml file.

This file has three sections you may need to edit:

Properties

The <vault.sdk.version> in your POM file must match the version of the Vault you are developing on. Note that when developing on limited release Vaults, the Vault Java SDK feature set is Beta and subject to change.

When Vault is upgraded to a new release or if you’re switching between Vaults during development, the <vault.sdk.version> element in the properties section must be updated accordingly to reimport the correct version of the Vault Java SDK from the repository.

You can find your Vault version in Admin > Settings > General Settings. You don’t need to worry about your Vault’s build number.

The <vault.sdk.version> must be in the following format:

[{vault_version}-release0, {vault_version}-release1000000]

For example, a Vault on version 18R3.0 should look like this:

<properties>
    <vault.sdk.version>[18.3.0-release0, 18.3.0-release1000000]</vault.sdk.version>
</properties>

Repositories

Your <repositories> section should look like this:

<repositories>
    <repository>
        <id>veevavault</id>
        <url>https://repo.veevavault.com/maven</url>
        <releases>
            <enabled>true</enabled>
            <updatePolicy>always</updatePolicy>
        </releases>
    </repository>
</repositories>

Dependencies

This dependency will pull the Vault Java SDK and all the libraries it depends on from the repository.

Your <dependencies> section should look like this:

<dependencies>
    <dependency>
        <groupId>com.veeva.vault.sdk</groupId>
        <artifactId>vault-sdk</artifactId>
        <version>${vault.sdk.version}</version>
    </dependency>
</dependencies>

Development Basics

Developing Vault Extensions means writing your own implementation of specific Vault extension interfaces, such as RecordTrigger or RecordAction. For example, a record trigger must implement the RecordTrigger interface and annotate the class with the @RecordTriggerInfo to provide deployment information.

The following is a skeleton code example of a trigger class implementation:

package com.veeva.vault.custom.triggers;

import com.veeva.vault.sdk.api.data.RecordTriggerInfo;
import com.veeva.vault.sdk.api.data.RecordTrigger;
import com.veeva.vault.sdk.api.data.RecordEvent;
import com.veeva.vault.sdk.api.data.RecordTriggerContext;
import com.veeva.vault.sdk.api.data.RecordChange;


@RecordTriggerInfo(object = "object_name__c", events = {RecordEvent.BEFORE_INSERT})
public class ObjectTrigger implements RecordTrigger {

    public void execute(RecordTriggerContext recordTriggerContext) {

        // process each input record.
        for (RecordChange recordChange : recordTriggerContext.getRecordChanges()) {
       }
    }
}

Generally, a Vault extension’s implementation uses services provided by the Vault Java SDK. With these services, you can apply custom business logic such as retrieving and performing data operations according to business requirements.

Learn more about services.

Programming Guidelines

While developing Vault extensions is essentially programming in Java, there are some language and JDK restrictions to ensure your code runs securely in Vault.

You should observe the following general guidelines when developing Vault extensions:

Code Validation

Restrictions are checked during validation, which happens when you deploy code to Vault from a VPK. For example, if your code uses a third-party library or non-allowlisted class, it will not pass validation and deployment will fail. We recommend validating your code often during the development process to catch issues early.

You can do this with the Validate Package endpoint.

POST /api/{version}/services/package/actions/validate

To use this endpoint, you must create a Vault Package File (VPK) as input.

You can view, download, delete, enable or disable deployed extensions in the Admin UI, located in Admin > Configuration > VAULT JAVA SDK. Learn more about the Admin UI in Vault Help.

Deploying Code

When testing Vault code locally through the debugger, the code is only active locally while the debugger is running. To make the code run for all users in your Vault, you must deploy it.

To deploy code, a Vault Admin must enable configuration packages in your Vault. Learn how to enable configuration packages in Vault Help.

Deploy code in three steps:

  1. Create a VPK with your source files
  2. Import the VPK to Vault
  3. Deploy the VPK

With this deploy method, you can accomplish any of the following:

If you need other deploy options, such as deploying or deleting a single file in the target Vault, see Managing Deployed Code. However, deploying a single file rather than a VPK is considered bad practice and should be used sparingly.

Note that your Vault user must have the correct permissions to deploy code. See the related permissions table for more information.

You can also create, validate, import, and deploy a VPK using the Vault Java SDK Maven Plugin.

Create a VPK

Before you can create your VPK, you must verify that your source code is in the proper file structure and prepare a valid vaultpackage.xml manifest file. This manifest file tells Vault whether you’re adding, replacing, or removing code.

Create a VPK by zipping your javasdk folder and the vaultpackage.xml manifest file and renaming it with the .vpk extension. For more detailed instructions, see Getting Started.

Verify File Structure

Your file structure must adhere to the following guidelines:

Create Manifest File

Your manifest file must be named vaultpackage.xml and must be located in the root of your file structure.

Example vaultpackage.xml:

<vaultpackage xmlns="https://veevavault.com/">
  <name>PKG-DEPLOY</name>
  <source>
    <vault></vault>
    <author>mmurray@veepharm.com</author>
  </source>
  <summary>PromoMats RecordTrigger</summary>
  <description>Record trigger on the Product object for PromoMats.</description>
  <javasdk>
    <deployment_option>incremental</deployment_option>
  </javasdk>
</vaultpackage>

All of the following attributes must appear in the manifest file. Attributes marked as Optional must still be included, but can be left with a blank value.

Attribute Description
<vaultpackage> Top-level attribute to hold all other attributes. Must include xmlns="https://veevavault.com/".
<name> A name which identifies this package.
<source> A top-level attribute to hold the following sub-attributes:
  • <vault>: Optional: We recommend leaving this blank. This is the Vault ID of the source Vault, but because you are importing this VPK, this attribute is ignored. When you export a VPK from Vault, this field is automatically populated with the source Vault ID.
  • <author>: The Vault user name of the user who created this package.
<summary> Provide more information about this package. Appears in the Summary section of Admin > Deployment > Inbound Packages.
<javasdk> A top-level attribute to hold the <deployment_option> sub-attribute. This tells Vault how to deploy your package in Vaut. Valid values are:
  • incremental
  • replace_all
  • delete_all
Learn more in the Deployment Options section.
<description> Optional: A description of your package. If omitted, the description will appear blank in Admin > Deployment > Inbound Packages.

Deployment Options

Import the VPK to Vault

After creating the VPK, you need to import this VPK to your Vault. This does not deploy the code, it just adds the VPK containing the code to your Vault. The following instructions import the VPK using the Vault REST API, but you can also import through the Vault UI.

With the Import Package endpoint, import your code.

PUT /api/{version}/services/package

The body of your request must include the VPK created in the previous step.

On SUCCESS, the response contains an id for the vaultPackage. You will need this ID to deploy the package through the API.

Deploy the VPK

After importing your VPK, you need to deploy it. This is the final step which makes your Vault extension run for all users. The following instructions deploy the VPK using the Vault REST API, but you can also deploy through the Vault UI. We recommend using the UI, which has a multi-step wizard that ensures validation.

Deploy your package with the Deploy Package endpoint.

POST /api/{version}/vobject/vault_package__v/{package_id}/actions/deploy

You can find the package_id URI Path Parameter in the API response from your import request. If you lost this ID, you can also find it in Admin > Deployment > Inbound Packages.

When you run the deploy endpoint, Vault first validates the VPK. If you have any validation errors, such as using non-allowlisted classes, deployment will fail. To avoid this, we recommend validating your package frequently throughout the development process.

After successful deployment, you can view deployed extensions in the Admin UI, located in Admin > Configuration > VAULT JAVA SDK. Learn more about the Admin UI in Vault Help.

Deployment Errors

If the deployment encounters any errors, Vault stops the deployment but does not roll back any changes it already made. We recommend downloading and checking the log file for details. Learn more about deployment errors in Vault Help.

Managing Deployed Code

Deploying VPKs are not the only way to manage your custom code. You can view, download, delete, enable or disable deployed extensions in the Admin UI, located in Admin > Configuration > VAULT JAVA SDK. Learn more about the Admin UI in Vault Help.

You also may need more granular deploy options. For example, you may need to delete a single file rather than all files. However, we do not recommend using the following single-file deploy methods as you may introduce or delete code which breaks existing deployed code. As a best practice, you should always use VPKs to manage code deployment.

Enable or Disable Extensions

When deployed, extensions are automatically enabled. You may wish to disable an extension if you are troubleshooting a bug, or loading data into a Vault and do not want a trigger to execute. You can easily enable and disable extensions through the Admin UI, or you can use the Vault REST API. Users must have the Admin: Configuration: Vault Java SDK: Create and Edit and permissions to enable or disable code.

PUT /api/{version}/code/{FQCN}/{enable || disable}

You can only enable and disable entry-point classes, such as triggers and actions. You cannot disable UDCs, or Vault extensions which reference other code components.

Download Source Code

You can retrieve the source code for a single file through the Admin UI, or through the Vault REST API. Users must have the Admin: Configuration: Vault Java SDK: Read permission to download source code.

GET /api/{version}/code/{FQCN}

Add or Replace Single Source Code File

You may need to add or replace a single file rather than a whole VPK. However, we do not recommend using the following single-file deploy method as you may introduce or delete code which breaks existing deployed code. As a best practice, you should always use VPKs to manage code deployment.

The following endpoint adds or replaces a single .java file in the currently authenticated Vault. If the given file does not already exist in the Vault, it is added. If the file does already exist, the file is updated.

PUT /api/{version}/code

Users must have the Admin: Configuration: Vault Java SDK: Create and Edit and permissions to use this endpoint.

Delete Single Source Code File

In some cases, you may need to delete a single file rather than replace all or delete all files. However, we do not recommend using the following single-file deploy method as you may introduce or delete code which breaks existing deployed code. As a best practice, you should always use VPKs to manage code deployment.

Code deletion is permanent. There is no way to retrieve a deleted code file. Vault does not allow deletion of a file which is currently in-use.

You can delete a single source file through the Admin UI, or through the Vault REST API.

DELETE /api/{version}/code

Users must have the Admin: Configuration: Vault Java SDK: Create and Edit and permission to delete code with this endpoint.

Triggers

Record Triggers

Understanding Record Triggers

A record trigger executes custom business logic whenever a data operation makes changes to an object record. Users manipulate data in Veeva applications by using the UI or API to Insert, Update, and Delete records. When these operations occur, the Vault Java SDK provides interfaces to interact with the record data before and after the data operations. Using the Java SDK, users can apply custom business logic in Event handlers for BEFORE and AFTER Events.

alt text

This Event-driven programming model allows developers to write small programs that target a specific object and Event to address common business requirements which standard application configurations cannot address.

The following are some typical uses for triggers by Event type:

BEFORE

Field Value Defaults: Default field values before creating a record.

Field Value Validations: Validate field values before saving or deleting a record.

Conditionally Required Fields: Make a field required by canceling the save operation if some condition is not met.

AFTER

Create, Update, or Delete Related Records: Create, update, or delete other records after saving or deleting a record.

Start Workflow: Start a workflow after creating or updating a record.

Change State: Change the lifecycle state of a record.

Illustration: Saving a New Record

Let’s examine a typical Save new record operation initiated by a user and walk through what a trigger does. When the user clicks the Save button, the system captures the object, such as product__v, and Event, such as BEFORE_INSERT, which looks up a registry for triggers on the given object and Event and executes them in order. The system passes the data entered by the user to the trigger during trigger execution.

BEFORE_INSERT trigger logic can interact with the current record to:

After saving the record, the system executes the AFTER_INSERT triggers for the same object.

AFTER_INSERT trigger logic can interact with the current record to:

Since the current record cannot change in the AFTER_INSERT Event, most of the business logic in this Event interacts with other records through RecordService or performs jobs on the current record that are executed asynchronously in a separate process. Learn more about the order of operations for object field defaults in Vault Help.

​Data Availability

When processing a request, the System performs the following sequence of steps:

  1. Execute BEFORE triggers.
  2. Write record changes to database.
  3. Update changes in VQL index.
  4. Execute AFTER trigger.

The data available in BEFORE and AFTER Event triggers depends on the operations (INSERT, UPDATE, and DELETE). For example, in an INSERT operation, you cannot get old or existing values because a new record is being inserted. Similarly, setting a field value only makes sense in the BEFORE Event in INSERT and UPDATE operations. It doesn’t make sense to set field ​value after it has been persisted or in a DELETE operation. The following chart illustrates when you can get or set field values.

Event

Record returned by getNew()

Record returned by getOld() 

 

getValue

setValue

getValue

setValue

BEFORE_INSERT

X

X

   

AFTER_INSERT

X

     

BEFORE_UPDATE

X

X

X

 

AFTER_UPDATE

X

 

X

 

BEFORE_DELETE

   

X

 

AFTER_DELETE

   

X

 

Query vs RecordService#readRecord

As illustrated above, BEFORE triggers can change field values, but these values are not persisted to the database and not updated in the VQL index yet. In this case, using the QueryService to retrieve a record being modified by a trigger will only return the old (existing) values. In order to get the values set by a trigger inside a transaction, you must use the RecordService#readRecord method. However, this method generally uses more memory. It is only recommended when you need to get field values modified by multiple triggers in a single transaction. Otherwise, we recommend QueryService to retrieve record data.

Because AFTER triggers happen after database updates and VQL indexing, you can use QueryService to retrieve both old and new values.

System Populated Fields

Lookup Field and Document Reference Field (latest version) are special field types. These field types have values set by the System.

In general, the System populates field values after the BEFORE Event. Because these field values are set by the System, the changes are not reflected in the BEFORE Event. For example, getNew() and getOld() will return the same existing value or null accordingly. However, the AFTER Event will return the new value set by the System in getNew() and the existing value in getOld().

In addition, because System-initiated requests do not fire triggers, triggers will not fire when the System updates a System-populated field.

If your trigger updates a document reference field, you must set the Document Version Reference to Specific Version. Learn more in Vault Help.

Anatomy of a Record Trigger

You can implement record triggers as normal Java classes. You can express complex business logic within a trigger class.

The code sample below explains the anatomy of a typical, basic trigger class. This example simply defaults a field value based on another field when creating a new record.

The explanations and line numbers below refer to the code sample above.

Line #1: Package

A custom record trigger must be under the com.veeva.vault.custom package. You can have further sub-package names as you see fit to organize your triggers. For example, you might use com.veeva.vault.custom.rim.submissions.triggers to indicate custom triggers for a RIM Submissions project.

Lines #3-11: Import

Only references to Vault Java SDK (com.veeva.vault.sdk.api.*) and a limited number of allowlisted classes, interfaces, and methods in the JDK are allowed. For example, String, LocalDate, List, etc.

Line #13: Annotation

The class annotation (@RecordTriggerInfo) indicates that this class is a record trigger. The annotation specifies the Object, Event(s), and Order of execution.

Line #14: Class Name

The class name declaration must include the public modifier and implements RecordTrigger. As a best practice, class name should indicate the object affected by this trigger and some functional description, for example, ProductFieldDefaults implements RecordTrigger means a trigger on Product that defaults some field values.

Line #16: execute() Method

You must implement this method for the RecordTrigger interface. This method has an instance of RecordTriggerContext passed in, so you can interact with the record(s) on which a user has initiated some operation.

Line #18: Context Record(s)

When a user performs a record operation whether by UI or API, such as creating a new record, the record being created is the context record. The operation may have multiple context records such as in a Bulk Create.

A list of records affected by the operation can be retrieved from RecordTriggerContext#getRecordChanges, and you can loop through each record to get field values and/or set field values. Your business logic is enclosed in this loop.

Line #21: getValue(String fieldName, ValueType.<T> fieldType)

You can retrieve field values from the context record. Trigger code operates as the System user, so no record level or field level security apply. All records and fields are accessible.

For new records, only new values are available. For updating records, both old and new values are available. The fieldName argument must be a valid field name in the object. For example, name__v. The fieldType argument must match the Vault field type in order to return the appropriate Java data type. Use the Data Type Map to find out how data types are mapped to objects in Vault.

Line #22: setValue(String fieldName, Object fieldValue)

You can set field value on fields that are editable. System fields, such as created_by__v and state__v, and Lookup fields are not editable.

The fieldName argument must be a valid field name in the object. The fieldValue argument must be an object of the appropriate data type for the field in the fieldName argument. Use the Data Type Map to find out how data types are mapped to objects in Vault.

Record Role Triggers

Understanding Record Role Triggers

A record role trigger executes custom business logic whenever roles are directly (manually) added or removed from an object record.

Users can manage manual role assignment in the UI with object sharing settings or from a workflow using the add or remove role system step, through the REST APIs, or using the Vault Java SDK RecordRoleService. Each of these methods can activate an SDK record role trigger.

When these operations occur, the Vault Java SDK provides interfaces to interact with the record data, and the record role change before and after the record role change. Using the Java SDK, users can apply custom business logic in event handlers for BEFORE and AFTER Events.

This event-driven programming model allows developers to write small programs that target a specific object and Event to address common business requirements that standard application configurations cannot address.

The following examples illustrate typical uses for record role triggers by Event type:

BEFORE

It is common practice to enforce validation rules on role assignment. The BEFORE Event allows the code to execute validation logic before the role assignments (or un-assignments) apply.

AFTER

Anatomy of a Record Role Trigger

You can implement record role triggers as normal Java classes. You can express complex business logic within a trigger class.

The code sample below explains the anatomy of a typical, basic trigger class.

Line #1: Package

package com.veeva.vault.custom.triggers.case3;

A custom record role trigger must be under the com.veeva.vault.custom package. You can have further sub-package names as you see fit to organize your triggers.

Lines #3-16: Import

import com.veeva.vault.sdk.api.core.RollbackException;

Only references to Vault Java SDK (com.veeva.vault.sdk.api.*) and a limited number of allowlisted classes, interfaces, and methods in the JDK are allowed. For example, String, LocalDate, List, etc.

Line #18: Annotation

@RecordRoleTriggerInfo(object="product__v", events={RecordRoleEvent.BEFORE, RecordRoleEvent.AFTER},
    order=TriggerOrder.NUMBER_2)

The class annotation (@RecordRoleTriggerInfo) indicates that this class is a record trigger. The annotation specifies the Object, Event(s), and order of execution.

Line #19: Class Name

public class CheckProductRole implements RecordRoleTrigger {

The class name declaration must include the public modifier and implements RecordRoleTrigger. As a best practice, class name should indicate the object affected by this trigger and some functional description, for example, CheckProductRole implements RecordRoleTrigger means a trigger on a Product that does validation on Role assignment.

Line #20: execute() method

public void execute(RecordRoleTriggerContext rroletc) {
        String ROLE_TO_READ = "owner__v";

You must implement this method for the RecordRoleTrigger interface. This method has an instance of RecordRoleTriggerContext passed in, so you can interact with the role record(s) on which a user has initiated some operation.

Line #27: getRecordRoleEvent()

// Get Record Role changes
        RecordRoleEvent event = rroletc.getRecordRoleEvent();

Get the record role Event from the RecordRoleTriggerContext. This trigger executes on both BEFORE and AFTER events, checking the context allows the code to check in which Event the code is executed.

Line #28

        List<RecordRoleChange> rrchanges = rroletc.getRecordRoleChanges();

The RecordRoleChanges() method return a list of RecordRoleChange. A recordRoleChange captures all changes done on the record role:

Line #37: getRecordRole()

// Get RecordRoleChange details

            RecordRole rrole = rrchange.getRecordRole();

A recordRoleChange provides a method getRecordRole() returning the current recordRole that is being changed. In a BEFORE Event, the recordRole exposes the role assigned before the change. In an AFTER Event, the recordRole exposes the role assignments after the change.

User Triggers

The Vaut Java SDK supports record triggers on the user__sys object. Users triggers work the same way as other record triggers, with some exceptions.

Most record triggers execute whenever a data operation makes changes to an object record. Unlike most record triggers, user triggers execute only when changes are made directly to the user__sys object record in a Vault. In other words, user triggers are not invoked by indirect updates.

For example, triggers on the user__sys object are not invoked in the following scenarios:

Trigger Execution & Performance

Execute as System

Custom code in Vault executes as the Java SDK Service Account, which has Vault Owner level access. Vault extension code, such as triggers and actions, can access object records with full read/write permission. This means any Vault user level, record level, or field level access restrictions do not apply. Custom code can copy or move data from object to object and delete data without regards to who the user is. It’s the developer’s responsibility to take that current user context into consideration and apply control where appropriate.

Data security should be considered when designing solutions using the Vault Java SDK.

Record Trigger Execution Flow

When a user initiates a request (INSERT, UPDATE, or DELETE) such as clicking Save in UI or sending a POST via Object API, the system processes the request by firing the BEFORE Event triggers first, then committing data to the database, and then firing the AFTER Event triggers.

BEFORE triggers are often used for defaulting field values and validating data entry, whereas AFTER Event triggers are mostly used to automate creating other records or starting workflow processes.

alt text

Record Role Trigger Execution Flow

When a user initiates a record role request, the system processes the request by firing the BEFORE Event triggers first, then committing data to the database, and then firing the AFTER Event triggers. BEFORE triggers are often used for validating data entry, whereas AFTER Event triggers are mostly used to automate the assignment or unassignment of roles on related records or documents.

Trigger Order and Nested Depth

A limit of 10 triggers are allowed in each Event and the order of execution can be specified. That means BEFORE and AFTER Events each have their own limit of 10 triggers allowed. In addition, when any given trigger executes, it can cause other triggers (nested triggers) to fire when it either performs a role a assignment or a data operation (INSERT, UPDATE, DELETE) programmatically. The nested trigger depth cannot exceed 10 levels deep.

To summarize, when a user initiates a request (for example, INSERT), the BEFORE Event triggers (up to 10) will execute in order. If any of the triggers cause other triggers to fire, the nested triggers will execute (up to 10 nested levels). After the system finishes the BEFORE triggers, the data with any changes made by the executed triggers persists, and the AFTER Event triggers will fire in the same manner with trigger order and nested depth. The image below illustrates this execution flow.

If you need to share data between different triggers or actions in the same transaction, you can do so with RequestContext.

System-Initiated Requests

Generally, triggers fire when a user initiates a request. When the System updates records, such as Lookup Field updates, triggers do not fire. Similarly, when the System performs a Hierarchical Copy (deep copy), the insert operation will not fire any triggers.

Terminating Execution

The trigger execution flow described above represents a transaction. In some cases, it is necessary to cancel the entire INSERT request and rollback any changes. Developers can throw a RollbackException in any trigger in the transaction, and execution will terminate immediately and roll back all changes.

Note that calling RecordChange#setError or RecordRoleChange#setError will not terminate a transaction. Instead, the trigger which caused the error will fail and the rest of the transaction will continue. In order to terminate an entire transaction, you should always throw a RollbackException.

The system will also terminate execution and rollback a request when errors occur, such as missing required field value on INSERT or exceeding allowed elapsed time limit (100 seconds).

Asynchronous Services

Calls to asynchronous services such as JobService or NotificationService will execute only when the request transaction completes. This way, you can use a RollbackException to stop the transaction if necessary, preventing asynchronous services from executing unintentionally when rolling back a transaction. For example, if a DELETE Event trigger calls NotificationService to send a notification, but a nested trigger later rolls back the transaction, the system should not delete the record nor send the notification. This prevents the asynchronous notification process from executing erroneously. Once the entire transaction completes successfully, all queued asynchronous services execute immediately.

Performance Considerations

Triggers should be designed to process records in bulk, especially when making service calls, such as QueryService, RecordService and RecordRoleService. These services are designed to take a list of records or record role changes as input for CRUD operations. It is much more efficient to build a list of record for input and make a single call to these services rather than make service calls one record at a time inside a loop.

Triggers that do not process records in bulk will perform poorly, especially when there are multiple triggers (including nested triggers), execution will likely exceed the maximum elapsed time (100s) or CPU time (10s) allowed. In addition, queries that return large number of records with large number of fields (including fields not used in your code) will likely exceed the maximum memory allowed (40MB).

Generally, you should never run a query or perform CRUD operations on records in a loop. Each iteration will make unnecessary service calls which can be easily batched to get the same result with a single service call.

Performance Example

The following poorly performing code executes a query inside a “for” loop, for each Product record in a request. That means when a request has multiple records, like from an API call or bulk update wizard, the QueryService#query call is made for each of the records. The only difference between each query is the WHERE clause contains a different Country reference field value. Performing multiple queries in this case is inefficient and time consuming. A better approach is to make a single query with a CONTAINS clause for each Country referenced by the Product records in the request.

To make performance even worse, as each query is executed to retrieve related records, a forEach loop is used to call RecordService.batchSaveRecords to save each new Country Brand record one at a time. Creating, updating, and deleting records are the most expensive and time-consuming operations. You should always batch records up in a list as input when calling batchSaveRecords.

While the better performing code requires more lines of code as illustrated below, it performs much better because it reduces data operations significantly by leveraging the Vault Java SDK’s interfaces to process records in bulk. 

Poorly Performing Code:
@RecordTriggerInfo(object = "product__v", events = RecordEvent.AFTER_INSERT)
public class ProductCreateRelatedCountryBrand implements RecordTrigger {
    public void execute(RecordTriggerContext recordTriggerContext) {

        for (RecordChange inputRecord : recordTriggerContext.getRecordChanges()) {

            QueryService queryService = ServiceLocator.locate(QueryService.class);
            String queryCountry = "select id, name__v from country__v where region__c=" + "'" + region + "'";
            QueryResponse queryResponse = queryService.query(queryCountry);

                queryResponse.streamResults().forEach(queryResult -> {
                Record r = recordService.newRecord("country_brand__c");
                r.setValue("name__v", internalName + " (" + queryResult.getValue("name__v", ValueType.STRING) + ")");
                r.setValue("country__c",queryResult.getValue("id",ValueType.STRING));
                r.setValue("product__c",productId);

                RecordService recordService = ServiceLocator.locate(RecordService.class);
                recordService.batchSaveRecords(VaultCollections.asList(r)).rollbackOnErrors().execute();

            });
        }

}
Better Performing Code:
@RecordTriggerInfo(object = "product__v", name= "product_create_related_country_brand__c", events = RecordEvent.AFTER_INSERT)
public class ProductCreateRelatedCountryBrand implements RecordTrigger  {

    public void execute(RecordTriggerContext recordTriggerContext) {

        // Get an instance of the Record service
        RecordService recordService = ServiceLocator.locate(RecordService.class);
        List<Record> recordList = VaultCollections.newList();

        // Retrieve Regions from all Product input records
        Set<String> regions = VaultCollections.newSet();
        recordTriggerContext.getRecordChanges().stream().forEach(recordChange -> {
            String regionId = recordChange.getNew().getValue("region__c", ValueType.STRING);
            regions.add("'" + regionId + "'");
        });
        String regionsToQuery = String.join (",",regions);

        // Query Country object to select countries for regions referenced by all Product input records
        QueryService queryService = ServiceLocator.locate(QueryService.class);
        String queryCountry = "select id, name__v, region__c " +
                "from country__v where region__c contains (" + regionsToQuery + ")";
        QueryResponse queryResponse = queryService.query(queryCountry);

        // Build a Map of Regions (key) and Countries (value) from the query result
        Map<String, List<QueryResult>> countriesInRegionMap = VaultCollections.newMap();
        queryResponse.streamResults().forEach(queryResult -> {
            String region = queryResult.getValue("region__c",ValueType.STRING);
            if (countriesInRegionMap.containsKey(region)) {
                List<QueryResult> countries = countriesInRegionMap.get(region);
                countries.add(queryResult);
                countriesInRegionMap.put(region,countries);
            } else
                countriesInRegionMap.putIfAbsent(region,VaultCollections.asList(queryResult));
        });

        // Go through each Product record, look up countries for the region assigned to the Product,
        // and create new Country Brand records for each country.
        for (RecordChange inputRecord : recordTriggerContext.getRecordChanges()) {

            String regionId = inputRecord.getNew().getValue("region__c", ValueType.STRING);
            String internalName = inputRecord.getNew().getValue("internal_name__c", ValueType.STRING);
            String productId = inputRecord.getNew().getValue("id", ValueType.STRING);

            Iterator<QueryResult> countries = countriesInRegionMap.get(regionId).iterator();

            while (countries.hasNext()){
                QueryResult country =countries.next();
                Record r = recordService.newRecord("country_brand__c");
                r.setValue("name__v", internalName + " (" + country.getValue("name__v", ValueType.STRING) + ")");
                r.setValue("country__c", country.getValue("id", ValueType.STRING));
                r.setValue("product__c", productId);
                recordList.add(r);
            }

        }

        // Save the new Country Brand records in bulk. Rollback the entire transaction when encountering errors.
        recordService.batchSaveRecords(recordList).rollbackOnErrors().execute();
    }
}

Custom Actions

Through the Vault Java SDK, you can create custom actions. These actions execute through the UI or API when invoked by a user. Unlike triggers, uploading action code does not make it execute. Action code requires an additional step from developers or Vault Admins to configure an action in Vault to use the uploaded code. Learn more about custom actions in the Javadocs.

Record Actions

Custom actions for records, called record actions, are invoked by a user on one or more specific records from the UI or API. You can configure custom record actions as any of the following:

Implementing Record Action

In order to implement a custom action, the RecordAction interface requires implementing the following two methods:

The @RecordActionInfo class annotation is also required to indicate this class is an action.

If your action updates a document reference field, you must set the Document Version Reference to Specific Version. Learn more in Vault Help.

Record Action Pre- and Post-Action UI Behavior

Optionally, a dialog can be shown in the UI before and after the action execution for a class that implements the RecordAction interface.

Pre- and post-action dialogs execute as separate transactions. Learn more in the Javadocs.

The following is a basic skeleton of a record action:

package com.veeva.vault.custom.actions;

        @RecordActionInfo(label="Say Hello", object="hello_world__c", icon="create__sys", usages={Usage.USER_ACTION, Usage.WORKFLOW_STEP})
        public class Hello implements RecordAction {
            // This action is available for configuration in Vault Admin.
            public boolean isExecutable(RecordActionContext context) {
                return true;
            }
            public void execute(RecordActionContext context) {
              //action logic goes here
            }
        }

Record Workflow Actions

Record workflow actions are custom actions which execute on an object or document workflow. Document workflows are a type of object workflow which are configured on the envelope__sys object.

If you are unfamiliar with object or document workflows, you should learn more before coding a record workflow action:

You can configure a custom action for object workflows on any of the following steps:

Once configured on a workflow step, the record workflow action is automatically invoked on all events for that workflow step during workflow execution.

You cannot start a workflow, create tasks, and cancel the new tasks in the same transaction. For example, you cannot start a workflow and then cancel all tasks in a TASK_AFTER_CREATE event.

Implementing Record Workflow Actions

A record workflow action is a Java class that implements the RecordWorkflowAction interface and has the @RecordWorkflowActionInfo annotation.

The RecordWorkflowAction interface must implement the execute() method. This is where you place the logic for your custom action. You can use any of the available Java SDK services to create logic for your action. For example:

Unlike record actions or document actions, the RecordWorkflowAction interface does not include an isExecutable() method. This means all record workflow actions are available for configuration once deployed. If you want to enable or disable a deployed record workflow action, you can do so in the UI or through the Vault REST API.

The @RecordWorkflowActionInfo annotation has the following elements:

The following is a basic skeleton of a record workflow action:

package com.veeva.vault.custom.actions;

        @RecordWorkflowActionInfo(label="Custom Approver", stepTypes={WorkflowStepType.START})
        public class CustomApprover implements RecordWorkflowAction {
            public void execute(RecordWorkflowActionContext context) {
                //action logic goes here
            }
        }

Document Actions

Along with the standard document actions you can configure in the Vault UI, you can create custom document actions using the Vault Java SDK to automate more specific business processes. Unlike document actions created through the Vault UI, custom document actions can run multiple sequential actions within one action, and can execute more complex conditional logic.

You can configure the following types of custom document actions:

You can find examples of document actions in our Sample Code.

Implementing Document Actions

A document action is a Java class that implements the DocumentAction interface and has the @DocumentActionInfo annotation.

The DocumentAction interface requires implementing the following two methods:

The @DocumentActionInfo class annotation requires the following:

Document Action Pre- and Post-Action UI Behavior

Optionally, a dialog can be shown in the UI before and after the action execution for a class that implements the DocumentAction interface.

Learn more about pre- and post-action dialogs in the Javadocs.

The following is a basic skeleton of a document action:

package com.veeva.vault.custom.actions;

        @DocumentActionInfo(label="Set Expiration", usage="LIFECYCLE_ENTRY_ACTION", icon="update__sys")
        public class SetDocumentExpiration implements DocumentAction {
         // This action is available for configuration in Vault Admin.
            public boolean isExecutable(DocumentActionContext context) {
                return true;
            }
            public void execute(DocumentActionContext context) {
                //action logic goes here
            }
        }

Action Icons

Choose which icon users see in the Action Bar for custom record and document actions by using the icon element of a RecordActionInfo or DocumentActionInfo annotation. If no icon is specified, the default value is "", meaning no icon is displayed and the action is not eligible to appear in the most frequently used section of the Action Bar. You can choose any of the following icons:

Label Value Icon
Create create__sys
Update update__sys
Delete delete__sys
Save save__sys
Remove remove__sys
Send send__sys
Import import__sys
Export export__sys
Generate generate__sys
Sync sync__sys

Debugging Actions

To debug action code, developers must deploy the code to Vault and configure a usage for the action. When the configured action is invoked through Vault, execution passes to the debugger to allow developers to step through the code. The code in your debugger will override any deployed code, allowing developers to test changes to a deployed action. Note that the class you wish to develop and debug must have the same package, class name, and annotation as the deployed code.

Services

While entry point interfaces like actions, triggers, and jobs define how and when Vault executes custom logic, services interfaces provide getter and setter methods that allow entry point implementations to interact with operations and data in Vault. For example, a RecordTrigger implementation might useJobService to start a workflow on a record, then use RecordService to update field values on the record. You can even create your own user-defined services.

Services are available to all entry point implementations.

Service Locator

To use a Vault service, you’ll first need to create an instance of that service using ServiceLocator.locate. The example below creates a new instance of JobService called jobService.

JobService jobService = ServiceLocator.locate(JobService.class);

Record Service & Document Service

Documents and object records are the most common types of data you’ll want to interact with. DocumentService and RecordService provide methods to create, update, and delete documents, document versions, and object records.

These services also allow creating, updating, and deleting attachments.

RecordService also allows setting record migration mode for a specific transaction. When migration mode is on, Vault bypasses entry criteria, entry actions, validation rules, event actions, and reference constraints when creating or updating records. While record migration mode is on, you cannot change an object’s type in the same transaction.

The trigger in the example below executes after Vault creates a vsdk_service_basics__c object record and uses RecordService to create two related records.

@RecordTriggerInfo(object = "vsdk_service_basics__c", events = {RecordEvent.AFTER_INSERT})
public class vSDKRecordService implements RecordTrigger {

    public void execute(RecordTriggerContext recordTriggerContext) {

        RecordEvent recordEvent = recordTriggerContext.getRecordEvent();
        RecordService recordService = ServiceLocator.locate(RecordService.class);
        List<Record> recordList =  VaultCollections.newList();
        RecordBatchSaveRequest.Builder recordSaveRequestBuilder = recordService.newRecordBatchSaveRequestBuilder();

        if (recordEvent.toString().equals("AFTER_INSERT")) {
            for (RecordChange inputRecord : recordTriggerContext.getRecordChanges()) {

                String name = inputRecord.getNew().getValue("name__v", ValueType.STRING);
                String id = inputRecord.getNew().getValue("id", ValueType.STRING);
                String relatedTo = inputRecord.getNew().getValue("related_to__c", ValueType.STRING);

                // Break out of the trigger code if the new record has a related "vsdk_service_basics__c" record.
                // This indicates that the records are "Copy of" records from "vSDKQueryService.java"
                // and do not need processing.
                if (relatedTo == "" || relatedTo == null) {

                    // Creates two related records by creating a new record via the RecordService.
                    // The name of records is set as "Related to: <name> x"
                    // The relation to the parent to then set with the "related_to__c" object reference field.
                    for (int i = 1; i <= 2; i++) {

                         Record r = recordService.newRecord("vsdk_service_basics__c");
                         r.setValue("name__v", "Related to: '" + name + "' " + i);
                         r.setValue("related_to__c", id);

                         recordList.add(r);
                    }
                }
            }

            recordSaveRequestBuilder.withRecords(recordList);
            RecordBatchSaveRequest saveRequest = recordSaveRequestBuilder.build();

            // If there are records to insert, the batchSaveRecords takes a RecordBatchSaveRequest as input.
            // This request should contain every new record that you are adding or updating.
            if (recordList.size() > 0) {
                recordService.batchSaveRecords(saveRequest)
                .onErrors(batchOperationErrors -> {

                    // Iterate over the caught errors.
                    // The RecordBatchOperation.onErrors() returns a list of BatchOperationErrors.
                    // The list can then be traversed to retrieve a single BatchOperationError and
                    // then extract an **ErrorResult** with BatchOperationError.getError().
                    batchOperationErrors.stream().findFirst().ifPresent(error -> {
                         String errMsg = error.getError().getMessage();
                         int errPosition = error.getInputPosition();
                         String name = recordList.get(errPosition).getValue("name__v", ValueType.STRING);
                         throw new RollbackException("OPERATION_NOT_ALLOWED", "Unable to create '" + 
                             recordList.get(errPosition).getObjectName() + "' record: '" +
                             name + "' because of '" + errMsg + "'.");
                     });
                })
                .execute();
            }
        }
    }
}

When retrieving values for formula fields, Vault returns the value of the field after running the formula expression, not the expression itself.

Intelligent Record & Document Update

Vault does not update object records or documents when a user or the Vault system saves without making any changes. This means that the Last Modified Date is not changed, record triggers do not execute, and Vault does not create an event in the object record or document audit history.

Lifecycle Services

The services in the lifecycle package provide methods that allow custom code to interact with object and document lifecycles.

Object Lifecycle Services

Object Lifecycle User Action Services

The Vault Java SDK supports initiating object lifecycle user actions, which are available to users on an object record in a specific lifecycle state. User actions can begin a workflow, move the record into a new lifecycle state, etc. Learn more about object lifecycle user actions in Vault Help.

The ObjectLifecycleStateUserActionMetadataService provides methods to retrieve configuration metadata about user actions, while ObjectLifecycleStateUserActionService provides methods to execute those actions.

Retrieving Configuration Metadata About Lifecycle User Actions

The following example illustrates how to retrieve configuration metadata about available lifecycle user actions.

 ObjectLifecycleStateUserActionMetadataService service = ServiceLocator.locate(ObjectLifecycleStateUserActionMetadataService.class);
      LogService logService = ServiceLocator.locate(LogService.class);

      // Retrieve metadata about user actions on a particular lifecycle state
      // Build the metadata request
      ObjectLifecycleUserActionMetadataRequest request = service.newLifecycleUserActionMetadataRequestBuilder()
          .withLifecycleName(lifecycleName)
          .withLifecycleState(lifecycleState)
          .withActionType("LIFECYCLE_RUN_WORKFLOW_ACTION") // Optional: filter the type of the user actions to be returned
          .build();

      ObjectLifecycleUserActionCollectionResponse response = service.getLifecycleUserActions(request);
      List<ObjectLifecycleUserActionMetadata> userActionMetadata = response.getUserActions();

      for (ObjectLifecycleUserActionMetadata data : userActionMetadata) {
          logService.info("User action metadata: label {}, component name {}, user action name {}," +
               "type {}, workflow name {}, record action class name {}, fully qualified name {}",
              data.getLabel(), data.getName(), data.getUserActionName(), data.getType(),
              data.getWorkflowName(), data.getRecordActionClassName(), data.getFullyQualifiedActionName());
     }

     // Retrieve metadata about user actions on an object record
     // Build the metadata request
     ObjectRecordLifecycleUserActionMetadataRequest request = service.newRecordLifecycleUserActionMetadataRequestBuilder()
         .withObjectName("object__c")
         .withRecordId("V6U000000001001")
         .withLifecycleState("inactive_state__c") // Optional: set the lifecycle state for the record, otherwise use the state the record is currently on
         .build();
     ObjectRecordLifecycleUserActionCollectionResponse response = service.getRecordLifecycleUserActions(request);
     List<ObjectRecordLifecycleUserActionDetail> details = response.getUserActions();

     for (ObjectRecordLifecycleUserActionDetail detail : details) {
         logService.info("User action is executable {}, is viewable {}", detail.isExecutable(), detail.isViewable());

         ObjectLifecycleUserActionMetadata metadata = detail.getMetadata();
         logService.info("User action metadata: label {}, component name {}, ...",
              metadata.getLabel(), metadata.getName());
     }

Retrieving Input Parameter Metadata for User Actions

The following example illustrates how to retrieve and use input parameter metadata for user actions. This service returns metadata about RecordActions annotated with a user_input_object, configured as a user action.

ObjectLifecycleStateUserActionService service = ServiceLocator.locate(ObjectLifecycleStateUserActionService.class);
      // Build the request
      ObjectLifecycleUserActionInputParameterMetadataRequest request = service.newInputParameterMetadataRequestBuilder()
          .withUserActionName("Objectlifecyclestateuseraction.object__c.active_state__c.action__c")
          .build();
      ObjectLifecycleUserActionInputParameterMetadata metadata = service.getInputParameterMetadata(request);

      // Get the metadata for the input parameter
      String userInputObjectName = metadata.getUserInputObjectName();
      String userInputObjectType = metadata.getUserInputObjectTypeName():

      // Use the metadata to create a record of that object
      Record input = recordService.newRecord(userInputObjectName);
      input.setValue("name__v", "new name");
      input.setValue("field__c", "new value");
      RecordBatchSaveRequest saveRequest = recordService.newRecordBatchSaveRequestBuilder()
          .withRecords(VaultCollections.asList(input))
          .build();
      recordService.batchSaveRecords(saveRequest)
          .rollbackOnErrors()
          .execute();
      String userInputObjectRecordId = input.getValue(RECORD_ID, ValueType.STRING);

      // Use the userInputObjectRecordId to build a request to execute an action with an input
      UserActionExecutionRequest request = userActionService.newExecutionRequestBuilder()
          .withObjectName("object__c")
          .withObjectRecordId("V6U000000001001")
          .withUserInputRecordId(userInputObjectRecordId)
          .withUserActionName("Objectaction.object__c.action__c") // Use the fully qualified name of the action
          .build();

Executing a Lifecycle User Action

The following example illustrates how to execute an action. An action can be a user action, such as a state change, or an object action that is either available on all lifecycle states or configured on a particular state.

The UserActionExecutionRequest.Builder is used to construct the request, which is submitted in a UserActionExecutionRequest. Optionally, you can specify success and error handlers on the UserActionExecutionOperation before execution.

  ObjectLifecycleStateUserActionService userActionService = ServiceLocator.locate(ObjectLifecycleStateUserActionService.class);
      LogService logService = ServiceLocator.locate(LogService.class);

      // Build user action execution request for the record
      UserActionExecutionRequest request = userActionService.newExecutionRequestBuilder()
          .withObjectName("object__c")
          .withObjectRecordId("V6U000000001001")
          .withUserInputRecordId(userInputObjectRecordId) // Optional: if the user action is a RecordAction annotated with a user_input_object.
          .withUserActionName("Objectaction.object__c.action__c") // Use the fully qualified name of the action.
          .build();

      // Execute the user action
      userActionService.executeUserAction(request)
          .onSuccess(userActionExecutionResponse -> {
              // Logic to be executed if the user action completes successfully.
              logService.info("Successfully executed user action");
           })
          .onError(error -> {
              // Logic to be executed if the user action encounters an error when executing.
              if (error.getErrorType().equals(ActionErrorType.PERMISSION_DENIED)) {
                  logService.error("Failed to execute user action: " + error.getMessage());
              }
          })
          .execute();

Document Lifecycle Services

Document Lifecycle User Action Services

The Vault Java SDK supports initiating document lifecycle user actions, which are available to users on a document in a specific lifecycle state. User actions can begin a workflow, move the document into a new lifecycle state, and more. Learn more about document lifecycle user actions in Vault Help.

DocumentLifecycleStateUserActionMetadataService provides methods to retrieve configuration metadata about user actions, while DocumentLifecycleStateUserActionService provides methods to execute those actions.

Retrieving Configuration Metadata About Lifecycle User Actions

The following example illustrates how to retrieve configuration metadata about available lifecycle user actions.

 DocumentLifecycleStateUserActionMetadataService service = ServiceLocator.locate(DocumentLifecycleStateUserActionMetadataService.class);
      LogService logService = ServiceLocator.locate(LogService.class);

      // Retrieve metadata about user actions on a particular lifecycle state
      // Build the metadata request
      DocumentLifecycleUserActionMetadataRequest request = service.newUserActionRequestBuilder()
          .withLifecycleName(lifecycleName)
          .withStateName(lifecycleState)
          .build();

      DocumentLifecycleUserActionMetadataResponse response = service.getUserActions(request);
      List<DocumentLifecycleUserActionMetadata> userActionMetadata = response.getUserActions();

      for (DocumentLifecycleUserActionMetadata data : userActionMetadata) {
          logService.info("User action metadata: label {}, user action name {}," +
               "workflow name {}, document action class name {}",
              data.getLabel(), data.getUserActionName(),
              data.getWorkflowName(), data.getDocumentActionClassName());
     }

     // Retrieve metadata about user actions on a document version
     // Build the metadata request
     DocumentVersionLifecycleUserActionMetadataRequest request = service.newDocumentVersionUserActionRequestBuilder()
         .withDocumentVersionId("1_2_3")
         .build();
     DocumentVersionLifecycleUserActionMetadataResponse response = service.getDocumentVersionUserActions(request);
     List<DocumentVersionLifecycleUserActionDetail> details = response.getUserActions();

     for (DocumentVersionLifecycleUserActionDetail detail : details) {
         logService.info("User action is executable {}, is viewable {}", detail.isExecutable(), detail.isViewable());

         DocumentLifecycleUserActionMetadata metadata = detail.getMetadata();
         logService.info("User action metadata: label {}, user action name {}, ...",
              metadata.getLabel(), metadata.getUserActionName());
     }

Retrieving Input Metadata for User Actions

The following example illustrates how to retrieve and use input metadata for user actions. This service returns metadata about DocumentActions annotated with a user_input_object.

 DocumentLifecycleStateUserActionMetadataService service = ServiceLocator.locate(DocumentLifecycleStateUserActionMetadataService.class);
      // Build the request
      DocumentLifecycleUserActionUserInputMetadataRequest request = service.newUserInputRequestBuilder()
          .withUserActionName("sdkDocLifecycleUA134c9hshe71tc__c")
          .build();
      DocumentLifecycleUserActionUserInputMetadata metadata = service.getUserInput(request);

      // Get the metadata for the input
      String userInputObjectName = metadata.getUserInputObjectName();
      String userInputObjectType = metadata.getUserInputObjectTypeName():

      // Use the metadata to create a record of that object
      Record input = recordService.newRecord(userInputObjectName);
      input.setValue("name__v", "new name");
      input.setValue("field__c", "new value");
      RecordBatchSaveRequest saveRequest = recordService.newRecordBatchSaveRequestBuilder()
          .withRecords(VaultCollections.asList(input))
          .build();
      recordService.batchSaveRecords(saveRequest)
          .rollbackOnErrors()
          .execute();
      String userInputObjectRecordId = input.getValue(RECORD_ID, ValueType.STRING);

      // Use the userInputObjectRecordId to build a request to execute an action with an input
      DocumentLifecycleUserActionExecutionRequest request = documentLifecycleStateUserActionService.newExecutionRequestBuilder()
          .withDocumentVersionId("1_2_3")
          .withUserInputRecordId(userInputObjectRecordId)
          .withUserActionName("sdkDocLifecycleUA134c9hshe71tc__c")
          .build();

Executing a Lifecycle User Action

The following example illustrates how to execute an action.

The DocumentLifecycleUserActionExecutionRequest.Builder is used to construct the request, which is submitted in a DocumentLifecycleUserActionExecutionRequest. Optionally, you can specify success and error handlers on the DocumentLifecycleUserActionExecutionOperation before execution.

DocumentLifecycleStateUserActionService userActionService = ServiceLocator.locate(DocumentLifecycleStateUserActionService.class);
      LogService logService = ServiceLocator.locate(LogService.class);

      // Build user action execution request for the document version
      DocumentLifecycleUserActionExecutionRequest request = userActionService.newExecutionRequestBuilder()
          .withDocumentVersionId("1_2_3")
          .withUserInputRecordId(userInputObjectRecordId) // Optional: if the user action is a DocumentAction annotated with a user_input_object.
          .withUserActionName("sdkDocLifecycleUA134c9hshe71tc__c")
          .build();

      // Execute the user action
      userActionService.executeUserAction(request)
          .onSuccess(userActionExecutionResponse -> {
              // Logic to be executed if the user action completes successfully.
              logService.info("Successfully executed user action");
           })
          .onError(error -> {
              // Logic to be executed if the user action encounters an error when executing.
              if (error.getErrorType().equals(ActionErrorType.PERMISSION_DENIED)) {
                  logService.error("Failed to execute user action: " + error.getMessage());
              }
          })
          .execute();

Workflow Services

The services in the workflow package provide methods that allow custom code to interact with object and document workflows. Document workflows are object workflows on the envelope__sys object.

The following is an example of a starting a workflow with the Vault Java SDK:

// Records to start Workflow with
String objectName = "object__c";
String record1 = "V5K000000001001";
String record2 = "V5K000000001002";
List<String> records = VaultCollections.asList(record1, record2);  // can also be list of documents if it's a document workflow

WorkflowMetadataService workflowMetadataService = ServiceLocator.locate(WorkflowMetadataService.class);

// 1) Get available workflows for given records
AvailableWorkflowMetadataCollectionRequest availableWorkflowsRequest = workflowMetadataService.newAvailableWorkflowMetadataCollectionRequestBuilder()
                                                                .withRecords(objectName, records)
                                                                .build();

AvailableWorkflowMetadataCollectionResponse response = workflowMetadataService.getAvailableWorkflows(availableWorkflowsRequest);

// Get list of available workflow for the given content listing
List<AvailableWorkflowMetadata> availableWorkflows = response.getWorkflows();

// Can loop over list of AvailableWorkflowMetadata to find/show all available workflows


// 2) Get Start step details to begin workflow
WorkflowStartMetadataRequest startMetadataRequest = workflowMetadataService.newWorkflowStartMetadataRequestBuilder()
                                                                    .withWorkflowName(workflowName)
                                                                    .withRecords(objectName, records)
                                                                    .build();

WorkflowStartMetadataResponse startMetadataResponse = workflowMetadataService.getWorkflowStartMetadata(startMetadataRequest);

// List of control sections in the start step
List<WorkflowStartStepMetadata> startStepMetadata = startMetadataResponse.getStartStepMetadataList();

for (WorkflowStartStepMetadata metadata : startStepMetadata){
    WorkflowStartStepType metaDataType = metadata.getType();  // Type of Start control (Participant, date, etc.)

    List<WorkflowParameterMetadata> parameters = metadata.getParameters(); // Parameters needed to start the workflow

    for (WorkflowParameterMetadata param : parameters) {
      String paramName = param.getName();                           // used in input parameter key
            WorkflowInputValueType valueType = param.getDataType();       // used to set the value of the control
        }
    }
}

// 3) Start workflow with these start inputs

String participantName = "viewer__c";
Long userId = 100l;
Long groupId = 500l;
List<String> userIds = VaultCollections.asList(userId.toString());
List<String> groupIds = VaultCollections.asList(groupId.toString());

String numberInput = "number__c";
int number = 123;

WorkflowInstanceService workflowInstanceService = ServiceLocator.locate(WorkflowInstanceService.class);

// Set workflow participants input
WorkflowParticipantInputParameter workflowParticipantInputParameter = workflowInstanceService.newWorkflowParticipantInputParameterBuilder()
            .withUserIds(userIds)
            .withGroupIds(groupIds)
            .build();

// Create Start instance request with the builder
WorkflowStartInstanceRequest startRequest = workflowInstanceService.newWorkflowStartRequestBuilder()
            .withWorkflowName(workflowName)
            .withRecords(objectName, records)
            .withInputParameters(numberInput, number)
            .withInputParameters(participantName, workflowParticipantInputParameter)
            .build();

// Start workflow with start request and success and error handlers
workflowInstanceService.startWorkflow(startRequest)
            .onSuccess(startResponse -> {
                String workflowId = startResponse.getWorkflowId();
                // Developer provided success handling logic here
            })
            .onError(startResponse -> {
                // Developer provided failure handling logic here
                if (startResponse.getErrorType() == ActionErrorType.INVALID_REQUEST) {
                    throw new RuntimeException(startResponse.getMessage());
                } else {
                    // ...
                }
            })
            .execute();  // needed to execute the action

Workflow Action Services

The Vault Java SDK supports initiating workflow actions and workflow task actions. These actions run on active workflow instances and are available in all Vaults. If you are unfamiliar with workflow actions, you can learn more in Vault Help:

You can find the workflow actions supported by the Vault Java SDK in the WorkflowActions Enum:

Annotation Services

The document viewer in the Vault UI allows users to mark up and comment on documents with a wide variety of annotations. Users can choose to create comment, line, link, or anchor annotations, and can reply to other annotations.

Learn more about document annotations in Vault Help.

Developers can retrieve annotation type metadata, read field values for individual annotations, edit or delete existing annotations, and create new annotations using the Vault Java SDK. Annotations are represented by the Vault Java SDK’s Annotation interface, which provides methods to retrieve annotation type names and field values. The nested Annotation.Builder interface creates an instance of Annotation and provides methods to set annotation field values when creating or updating an annotation.

Custom code can interact with annotations using the following services:

Annotation Types

Users can create the following types of annotations:

PromoMats Vaults include additional annotation types related to Text and Claims Management functionality. Some of these types can’t be created directly by users, but are instead created by Vault in response to the Suggest Links user action. Learn more about Using Text and Claims Management in Vault Help.

Annotation Fields

Each annotation may contain the following fields:

Annotation Placemarks

A placemark defines where on a document an annotation appears. For example, a text selection, a page number, or the coordinates of an area selection. The Vault Java SDK’s AnnotationPlacemark interface provides methods to set and retrieve type names and field values for annotation placemarks.

Placemark coordinates are listed in the style of PDF quad points and are measured in pixels from the bottom-left of the page at 72 DPI. There will be 8*n coordinates, where n is the number of rectangles that make up the placemark.

There are several types of placemarks:

Annotation placemarks include the following fields:

Annotation References

A reference is a way for an annotation to refer to an external entity. The Vault Java SDK’s AnnotationReference interface provides methods to set and retrieve type names and field values for annotation references.

There are three types of annotation references:

Annotation references include the following fields:

See example code and learn more about the interfaces in the annotations package in the Javadocs.

Vault Information Service & Log Service

When creating custom code, you may need to retrieve basic configuration information about your Vault. The VaultInformation interface provides methods to retrieve the following information about the local Vault:

The VaultInformationService interface provides methods to retrieve the VaultInformation for remote and local Vaults.

Logging is an essential part of any application, and the LogService interface provides methods to send messages to the Debug Log and Runtime Log, which are available in the Vault UI from Admin > Logs > Vault Java SDK Logs.

The following example creates a new instance of Vault Information Service and uses VaultInformationService’s getLocalVautInformation() method to retrieve the local Vault’s information. Next, the example uses LogService to log the dns, id, name, language, locale, and timezone values in the Debug Log.

LogService logService = ServiceLocator.locate(LogService.class);
VaultInformationService vaultInformationService = ServiceLocator.locate(VaultInformationService.class);
VaultInformation vaultInformation = vaultInformationService.getLocalVaultInformation();
logService.debug("dns = {}", vaultInformation.getDNS());
logService.debug("id = {}", vaultInformation.getID());
logService.debug("name = {}", vaultInformation.getName());
logService.debug("language = {}", vaultInformation.getLanguageCode());
logService.debug("locale = {}", vaultInformation.getLocaleCode());
logService.debug("timezone = {}", vaultInformation.getTimezoneName());

Token Service

TokenService provides methods to define and resolve Custom tokens. You can use these tokens with:

You can define custom tokens using TokenRequest.Builder as shown in the example below.

TokenService tokenService = ServiceLocator.locate(TokenService.class);
      TokenRequest.Builder builder = tokenService.newTokenRequestBuilder();

      builder.withValue("Custom.api_version", "1.0");
      builder.withValue("Custom.is_case_sensitive", Boolean.TRUE);
      builder.withValue("Custom.max_amount", new BigDecimal(100));

      TokenRequest tokenRequest = builder.build();

Query Service

QueryService provides methods to create, execute, validate, and count query requests.

For example, imagine a simple VQL query to return the name and ID of all documents where the product is Cholecap.

SELECT name__v, id
FROM documents
WHERE product__v = '00P000000000119'

The example below creates a new instance of QueryService called queryService, then uses the Query Builder to create the same query as shown above.

QueryService queryService = ServiceLocator.locate(QueryService.class);
Query myQuery= queryService.newQueryBuilder()
.withSelect(VaultCollections.asList("name__v", "id"))
.withFrom("documents")
.withWhere("product__v = '00P000000000119'")
.build();

Other interfaces in the query package also provide builders that you can use with a defined query. You can use QueryExecutonRequest to execute the query, QueryValidationRequest to validate the query, or QueryCountRequest to retrieve the total number of records the query returns without a full query execution.

Use QueryOperation to execute a query and to specify success and error handlers. Vault returns query results as instances of QueryExecutionResponse. You can retrieve individual field values using QueryExecutionResult.getValue(). When querying formula fields, Vault returns the value of the field after running the formula expression, not the expression itself.

Using Tokens in a Query

You can include Custom tokens and Vault tokens in queries.

QueryCountRequest.Builder, QueryExecutionRequest.Builder, and QueryValidationRequest.Builder provide withTokenRequest() methods which accept a TokenRequest and a QueryEscapeOverride. If you omit overrides, Vault escapes all Strings.

The following example uses the Query Builder to create a query containing two custom tokens, status_list and approved_date. It defines these tokens using the Token Request Builder to create a TokenRequest. Finally, it uses the Query Execution Request Builder to include both the Query and Token requests in a QueryExecutionRequest.

Query query = queryService.newQueryBuilder()
      .withSelect(asList("id"))
      .withFrom("documents")
      .withWhere("TONAME(status__v) CONTAINS (${Custom.status_list})")
      .appendWhere(QueryLogicalOperator.AND,
            "approved_date__c > ${Custom.approved_date}")
      .withMaxRows(50)
      .withOrderBy(asList("name__v"))
      .build();

TokenService tokenService = ServiceLocator.locate(TokenService.class);
TokenRequest tokenRequest = tokenService.newTokenRequestBuilder()
      .withValue("Custom.approved_date", LocalDate.now().minusDays(7))
      .withValue("Custom.status_list", asList("final__c","approved__c"))
      .build();

QueryExecutionRequest queryRequest = queryService
      .newQueryExecutionRequestBuilder()
      .withQuery(query)
      .withTokenRequest(tokenRequest)
      .build();

Vault resolves tokens in query responses, but not in error responses or when retrieving query strings with Query#getQueryString().

The following limitations apply:

Learn more in the Javadocs.

Using Record Properties

Record properties provide more information about a record than a query response normally provides. For example, you can retrieve a List of editable fields or the target of a hyperlink in a formula field.

You can request record properties using the QueryExecutionRequest#withQueryRecordPropertyTypes method, which accepts a List of QueryRecordPropertyType values. For example, the HIDDEN type provides a List of fields that are hidden due to Atomic Security, and the RECORD_PERMISSIONS type provides the Edit, Delete, Create, and Read permissions for the record.

Each property type has its own response object containing additional data about a record and methods to retrieve that data. For example, the QueryRedactedFieldResult response object for the REDACTED type provides the #getFieldNames() method to retrieve the names of redacted fields.

The following example uses QueryService and LogService to log record properties for my_object__c. The QueryHiddenFieldResult#getFieldNames() method checks whether possible_hidden_field__c is hidden. The QueryWeblinkResult#getWeblinks() method retrieves the target attribute for Link formula field my_weblink__c.

LogService logService = ServiceLocator.locate(LogService.class);
QueryService queryService = ServiceLocator.locate(QueryService.class);

QueryExecutionRequest request = queryService
    .newQueryExecutionRequestBuilder()
    .withQueryString("SELECT id, possible_hidden_field__c, my_weblink__c FROM my_object__c")
    .withQueryRecordPropertyTypes(VaultCollections.asList(QueryRecordPropertyType.HIDDEN, QueryRecordPropertyType.WEB_LINK))
    .build();

queryService.query(request)
    .onSuccess(response -> {
        response.streamResults().forEach(row -> {
            String id = row.getValue("id", ValueType.STRING);
            boolean fieldIsHidden = row
                .getTypedQueryRecordProperty(QueryHiddenFields.class)
                .getFieldNames()
                .contains("possible_hidden_field__c");

            QueryWebLinkTarget webLinkTarget = row
                .getTypedQueryRecordProperty(QueryWeblinkFields)
                .getWeblinks()
                .stream()
                .filter(w -> "my_weblink__c".equals(w.getFieldName()))
                .findFirst()
                .map(QueryWeblinkResult::getTarget).orElse(null);

            logService.info(String.format("Record: [%s], hidden: [%s], web link target: [%s]", id, fieldIsHidden, webLinkTarget));
        });
    })
    .onError(error -> {
        logService.error(error.getMessage());
    }).execute();

Learn more in the Javadocs.

Job Service

JobService provides methods to execute tasks asynchronously using jobs. You can also create job processors using the Job interface.

See the Javadocs to learn more about the other interfaces in the job package and to see the example code.

The following limitations apply to SDK jobs per Vault: * The maximum chunk size is 500 JobItem instances. * The maximum size of a JobTask is 128 KB. * The maximum number of JobTask instances is 5000. * The maximum size of JobParameters is 8 KB. * The maximum number of JobParameters instances is 8000. * The maximum number of Job queues is 25. * The maximum number of queued SDK Job instances is 1000.

Creating Job Processors

Job processors provide logic to process jobs in bulk. You can invoke a job processor using triggers and actions in Vault Java SDK code. A job processor can also form the logic for a new job definition. Learn more about job definitions in Vault Help.

You can use the Job interface with the following methods to create a job processor:

You must associate your custom job processor with a job by adding it to the Job Code field of an SDK Job Metadata record, which corresponds to the job_code field of the Jobmetadata component. Learn more about administering SDK job metadata in Vault Help.

The following is a basic skeleton of a job processor:

@JobInfo(adminConfigurable = true, idempotent = true, isVisible = true)
public class CustomSdkJob implements Job {

    public JobInputSupplier init(JobInitContext context) {

    }

    public void process(JobProcessContext context) {

    }

    public void completeWithSuccess(JobCompletionContext context) {

    }

    public void completeWithError(JobCompletionContext context) {

    }
}

Executing Jobs

You can use JobService to initiate a job from custom Vault Java SDK code.

To initiate a job from code, you must first define the job parameters. The JobParameters interface provides methods to set the start time, title, and parameter values for the job.

To set a job parameter value, you can pass a String to JobParameters#setValue. Alternatively, the JobParamValue interface allows you to create more complex job parameter values. For example, the following code creates a custom job parameter value for record IDs:

@UserDefinedClassInfo()
public class CustomSdkJobParameters implements JobParamValue {

    private List<String> recordIds;

    public List<String> getRecordIds() {
        return recordIds;
    }

    public void setRecordIds(List<String> recordIds) {
        this.recordIds = recordIds;
    }
}

After defining the job parameters, identify the job to run by passing the name of the Jobmetadata component for the job to the JobService#newJobParameters method.

The following example identifies the job as custom_sdk_job__c and sets the job parameters with the user-defined CustomSdkJobParameters value. It then calls JobService#run to start the job 2 minutes after invocation. This calls the job processor associated with custom_sdk_job__c.

public void updateRecordsAsynchronously(List<RecordChange> recordChanges) {

    JobService jobService = ServiceLocator.locate(JobService.class);

    List<String> recordIds = VaultCollections.newList();

    recordChanges.forEach(recordChange -> recordIds.add(recordChange.getNew().getValue("id", ValueType.STRING)));

    if (!recordIds.isEmpty()) {
        JobParameters jobParameters = jobService.newJobParameters("custom_sdk_job__c");

        CustomSdkJobParameters customSdkJobParameters = new CustomSdkJobParameters();
        customSdkJobParameters.setRecordIds(recordIds);

        jobParameters.setValue("custom_parameters", customSdkJobParameters);
        jobParameters.setJobRunTime(ZonedDateTime.now().plusMinutes(2));

        jobService.run(jobParameters);
    }
}

Additional Services

In addition to the above services, the Vault Java SDK provides the following:

Document Services

Object Services

Integration Services

Other Services

Learn more in the Javadocs. For more code examples using services, see our sample code.

Request Context

The RequestContext interface provides access to the context of a transaction. This allows you to pass data from the initial firing to subsequent triggers within the same transaction.

For example, initiating a request from an action which causes other triggers to execute, including nested triggers. As triggers execute through a thread of execution, some triggers along the execution sequence may need context from previously executed logic. This is especially needed when executing the same trigger multiple times within the same request transaction, for example, once for the current record, then subsequent execution in nested triggers on the same object. The subsequent firing may need to run different logic than the initial firing.

You can share some data using RequestContext to set a named context and get the named context value anywhere along a request transaction in downstream triggers. The maximum amount of data you can share is 5 MB per transaction request. The value you can set must be one of the value types specified in RequestContextValueType, or an implementation of the RequestContextValue interface.

Note that a value stored in RequestContext requires an explicit getValue and setValue whenever you want to change the value. If you change the state of your RequestContextValue object, you must call setValue to put the mutated object back into the RequestContext.

To properly debug uses of RequestContext, all code that uses the context should be in the debugger. If not, the value of the context may be inaccurate, especially if you have a context value set by code already deployed to Vault and that code is absent from your debugger. Any code that uses getValue and setValue should be in your debugger.

Using RequestContext

The following is an example of using RequestContext. First, a trigger named ProductBuildRegionMap sets up a RequestContext:

@RecordTriggerInfo(object = "product__v", name = "product_region__c", events = {RecordEvent.BEFORE_INSERT})
public class ProductBuildRegionMap implements RecordTrigger {
   public void execute(RecordTriggerContext recordTriggerContext) {
       List<String> productRegions = VaultCollections.newList();

       recordTriggerContext.getRecordChanges().stream().forEach(recordChange ->
          productRegions.add(recordChange.getNew().getValue("region__c",ValueType.STRING)));

       // Create a new region country map for all regions in this request and set the map into the "regionCountryMap"
       // request context, so that this map can be used by other triggers that execute after this one.
       RegionCountryMap regionCountryMap = new RegionCountryMap(productRegions);

       RequestContext.get().setValue("regionCountryMap", regionCountryMap);
   }
}

Next, we have a user-defined class which implements the RequestContextValue set up in ProductBuildRegionMap.

@UserDefinedClassInfo(name = "regioncountrymap__c")
public class RegionCountryMap implements RequestContextValue {

   private Map<String, List<String>> regionCountryMap = VaultCollections.newMap();

   RegionCountryMap (List<String> productRegionId){
       // Constructor to create a regionCountryMap <region, countries> by querying the Region object
       // to retrieve countries in the provided regions.
   }
   Map<String, List<String>> getMap () {
       return regionCountryMap;
   };
}

Lastly, our ProductCreateRelatedBrands trigger executes after the ProductBuildRegionMap trigger. It retrieves the RequestContextValue and modifies the Map inside the context.

@RecordTriggerInfo(object = "product__v", name = "product_region__c", events = {RecordEvent.AFTER_INSERT})
public class ProductCreateRelatedBrands implements RecordTrigger {

   public void execute(RecordTriggerContext recordTriggerContext) {
       // Get the RegionCountryMap from the request context
       RegionCountryMap regionCountryMap = RequestContext.get()
               .getValue("regionCountryMap", RegionCountryMap.class);
       // Get the map of regions and countries
       Map<String, List<String>> map = regionCountryMap.getMap();

       // ... create specific brands for each country in a region

       // Remove a region from the map if brands for that region already exists and set the map back to the
       // request context in order to update request context with the changed map.
       map.remove("Asia");

       RequestContext.get().setValue("regionCountryMap", regionCountryMap);
   }
}

Request Context User Types

RequestContext supports the following user types, which are listed in the RequestContextUserType Enum:

HttpService and ConnectionService each provide methods to determine if a request will succeed with a specified user type and to create a new request with a specified user type.

Request Execution Context

RequestExecutionContext provides contextual information about the current request. The following example determines if the current request takes place within a workflow and, if it does, retrieves the workflow ID, workflow name, and workflow owner ID. Learn more in the javadocs.

LogService logService = ServiceLocator.locate(LogService.class);

if (RequestContext.get().containsRequestExecutionContext(WorkflowRequestExecutionContext.class)) {
   WorkflowRequestExecutionContext workflowRequestExecutionContext = RequestContext.get().getRequestExecutionContext(WorkflowRequestExecutionContext.class);

   logService.debug("workflowId = {}", workflowRequestExecutionContext.getWorkflowId());
   logService.debug("workflowName = {}", workflowRequestExecutionContext.getWorkflowName());
   logService.debug("workflowOwnerId = {}", workflowRequestExecutionContext.getWorkflowOwnerId());
}
else {
   logService.debug("Not in a workflow");

User-Defined Classes

User-defined classes (UDC) allow you to implement reusable logic into a single class, rather than repeating the same logic across multiple triggers on different objects. User-defined classes are then used by Vault extensions, such as triggers and actions. Developers can also use UDCs as an object to store complex data.

You can use UDCs to apply object-oriented solution designs by having interfaces, abstract classes, and class implementations in separate UDCs.

Unlike Vault extensions which execute when a user or the System initiates an operation, UDCs only execute by calls from other classes.

UDCs can use any of the following libraries and services:

Creating User-Defined Classes

A user-defined class is a Java class which uses the @UserDefinedClassInfo class annotation. For example, the following illustrates a user-defined class ValidationUtils:

@UserDefinedClassInfo
public class ValidationUtils {
    boolean isNameFormatted (Record record) {
        String name = record.getValue("name__v", ValueType.STRING);
        if (name.length() < 100 && !name.substring(0,2).equals("BAC"))
            return true;
        else
            return false;
  }
}

Using User-Defined Classes

You can use a UDC in any Vault Java SDK extension, such as a trigger or an action class, as well as other UDCs. The following example illustrates a trigger using the ValidationUtils user-defined class:

@RecordTriggerInfo(object="product__v", events={RecordEvent.BEFORE_INSERT})
public class Example implements RecordTrigger {
    public void execute(RecordTriggerContext recordTriggerContext) {
        ValidationUtils validationUtils = new ValidationUtils();
        for (RecordChange inputRecord :
            recordTriggerContext.getRecordChanges()) {
                if (!validationUtils.isNameFormatted(inputRecord)){
                    // set Name field to format required for this object
                }
            }
    }
}

While debugging a trigger or action, you can step into UDCs to debug them. Because UDCs are not directly executable, you must step into them when calling them from an extension class.

User-Defined Models

User-defined models (UDM) allow you to create reusable data access objects, or models, and annotate their getters and setters as user-defined properties. You can then use models with JsonService to translate data to and from JSON, or with HttpService to send and receive data using REST APIs.

With user-defined models, you can:

Creating User-Defined Models

A user-defined model is an interface that uses the @UserDefinedModelInfo annotation and extends the UserDefinedModel interface. For example, the following is an example of a user-defined model that represents three fields, id, name__v, and product__v on a single document.

@UserDefinedModelInfo(include = UserDefinedPropertyInclude.NON_NULL)
public interface BasicDocModel extends UserDefinedModel {

   @UserDefinedProperty()
   BigDecimal getId();
   void setId(BigDecimal id);

   @UserDefinedProperty(name = "name__v", aliases = {"name", "name__c"})
   String getName();
   void setName(String name);

   @UserDefinedProperty(name = "product__v", include = UserDefinedPropertyInclude.ALWAYS)
   ProductModel getProduct();
   void setProduct(ProductModel product);

}

A user-defined model can extend another user-defined model. For example, a PresentationDocModel could extend the BasicDocModel.

About User-Defined Properties

The @UserDefinedProperty annotation can only be used on an interface with the @UserDefinedModelInfo annotation. All @UserDefinedProperty annotations must have a defined getter method with no parameters and a valid return type. Setter methods must have one parameter of a valid type. See the Javadoc for valid types for getters and setters. The name parameter is the JSON field name that will be serialized from and deserialized to this property. If the JSON field name is not always the same, you can define one or more aliases to be deserialized to this property. If the JSON field name value exactly matches the getter and setter name pattern, the name parameter can be omitted as shown for getId() and setId() below.

You can set one or more default values using the defaultValue or defaultValues parameter. You can only set default values for properties whose type is BigDecimal, Boolean, String, or a collection of Strings.

@UserDefinedProperty(name = "pageoffset", aliases = {"offset"})
BigDecimal getPageOffset();

@UserDefinedProperty(defaultValue = "New Document")
String getName();

@UserDefinedProperty
BigDecimal getId();
void setId(BigDecimal id);

About UserDefinedPropertyInclude

The UserDefinedPropertyInclude enum specifies how JsonService should serialize values and is set by the include parameter of a user-defined model or property. If no value is set for a user-defined property, serialization defaults to the behavior of the user-defined model it belongs to. During deserialization, JsonService ignores any fields that are not defined in the user-defined model. Learn more in the Javadoc.

@UserDefinedModelInfo(include = UserDefinedPropertyInclude.ALWAYS)
public interface MyModel extends UserDefinedModel {

@UserDefinedProperty(include = UserDefinedPropertyInclude.NON_NULL)
String getData();
}

Using User-Defined Models

Use UserDefinedModelService to create a new, empty model instance in a trigger or action. You can then use JsonService to serialize and deserialize data. You can also use HttpService to create an HTTP request that accepts one or more user-defined models as the request body, or returns a user-defined model as the response body.

UserDefinedModelService modelService = ServiceLocator.locate(UserDefinedModelService.class);
BasicDocModel docModel = modelService.newUserDefinedModel(BasicDocModel.class);

JsonService jsonService = ServiceLocator.locate(JsonService.class);
String newJSON = jsonService.convertToString(docModel);

Once deployed, user-defined models are visible in the Vault UI from Admin > Configuration > User-Defined Models.

User-Defined Services

User-defined services (UDS) allow you to wrap reusable logic into a service that can be used by other Vault Java SDK code, such as triggers, actions, or user-defined classes. Much like Vault Java SDK services, UDS are stateless singletons whose methods, once exited, return their allocated memory to the maximum memory execution limit: only the method parameters and return value counts toward the memory limit. Since UDS are stateless, nothing is retained beyond the service method execution.

UDS only execute by calls from other classes, differentiating them from Vault extensions that execute when a user or the system initiates an operation.

Locate the user-defined service using ServiceLocator, just like Vault Java SDK services. Once located, execute the service with a normal Java method call.

User-Defined Services vs. User-Defined Classes

Unlike user-defined classes, the memory UDS use in their methods is freed up when the method exits. Only the method return value counts towards the total SDK extension memory limit. However, it is important to note that just like SDK services, UDS do not store any data after the method exits. If data needs to be stored, or if common methods need to be shared between triggers or actions, a user-defined class should be used.

Using User-Defined Services

To use a UDS, extend UserDefinedService and create a class implementing the UserDefinedService extension. The following examples illustrate how to perform a product search with a UDS.

Create a new service by extending UserDefinedService and using the @UserDefinedServiceInfo annotation.

@UserDefinedServiceInfo
public interface ProductSearchClass extends UserDefinedService {
    boolean doesProductExist();
}

Implement the new interface that includes reusable code.

@UserDefinedServiceInfo
public class ProductSearchClassImpl implements ProductSearchClass {
    @Override
    public boolean doesProductExist() {
        // Reusable code
        return true;
    }
}

Locate the service and execute method from a Vault extension such as a trigger or an action.

ProductSearchClass productSearchClass = ServiceLocator.locate(ProductSearchClass.class);
boolean productExists = productSearchClass.doesProductExist();

Debugging User-Defined Services

When the SDK debugger is attached to a Vault session, the UDS executes on the client side, just like a user-defined class. Therefore, for any SDK invocations the service makes, the size of the objects returned by the invocations counts towards memory usage. The memory used by those objects is not decremented when the service method exits.

Objects returned by the service method do not count towards the Vault extension memory limit when the SDK debugger is attached to a Vault session.

Email Processors

When creating Inbound Email Addresses in the Vault Admin UI, Admins select an Email Processor, which defines how Vault automatically creates documents, records, and attachments from emails sent to the address. In addition to those provided by the System, you can create custom Email Processors with the Vault Java SDK. Learn more about Email to Vault in Vault Help.

Creating Email Processors

An email processor is a Java class that implements the EmailProcessor interface and uses the @EmailProcessorInfo annotation. Additionally, email processors use:

Learn more in the Javadocs.

About EmailProcessorInfo

The EmailProcessorInfo annotation defines the senders from whom the email processor will accept emails. In this example, the processor accepts emails from one or more specific groups defined in the Inbound Email Address configuration. The label element defines the value Vault displays for the processor in the Admin UI.

@EmailProcessorInfo(allowedSenders = EmailSenderType.VAULT_GROUPS, label = "Create Legal Document")

Creating Documents From Emails

The following example creates a document using a custom document type called Email Inquiry Document, but you can use any standard or custom document type in your Vault. The processor gets an email item, then uses DocumentService to create a document and set the source file and field values. If the Allow attachments option is enabled for the specified document type, Vault automatically creates document attachments from any files attached to an email.

@EmailProcessorInfo(allowedSenders = EmailSenderType.VAULT_USERS, label = "Create Email Inquiry Document")
public class EmailInquiryProcessor implements EmailProcessor {

    @Override
    public void execute(EmailProcessorContext context) {

        EmailItem emailItem = context.getEmailItem();
        DocumentService documentService = ServiceLocator.locate(DocumentService.class);
        DocumentVersion documentVersion = documentService.newDocument();
        documentVersion.setSourceFile(emailItem.getEmailFile());
        documentVersion.setValue("name__v", emailItem.getSubject());
        documentVersion.setValue("type__v", VaultCollections.asList("email_inquiry__c"));
        documentVersion.setValue("lifecycle__v", VaultCollections.asList("general_lifecycle__c"));
        documentVersion.setValue("status__v", VaultCollections.asList("draft__c"));
        documentService.createDocuments(VaultCollections.asList(documentVersion));
    }
}

Creating Object Records From Emails

The example below uses RecordService to create an object record from an email. Instead of getting the email source file, this processor first calls getEmailBodySize to ensure that the body can be loaded into memory, then uses getEmailBody() from EmailService to copy the body of the email to a Long Text field, email_body__c, as plain text. If the body exceeds 40mb (4,000,000 b), the email processor instead sets the field to an “Email body exceeds maximum size allowed” message.

@EmailProcessorInfo(allowedSenders = EmailSenderType.VAULT_USERS, label = "Create Object Record")
public class ObjectEmailProcessor implements EmailProcessor {

   @Override
   public void execute(EmailProcessorContext context) {

       EmailItem emailItem = context.getEmailItem();
       EmailService emailService = ServiceLocator.locate(EmailService.class);
       RecordService recordService = ServiceLocator.locate(RecordService.class);
       Record record = recordService.newRecord("email_to_object__c");
       record.setValue("name__v", emailItem.getSubject());
       record.setValue("sender__c", emailItem.getSender().getEmailAddress().getFullAddress());
       int bodysize = emailItem.getEmailBodySize(EmailBodyType.TEXT_PLAIN);
       if (bodysize < 4000000) {
           record.setValue("email_body__c", emailService.getEmailBody(emailItem.getId(), EmailBodyType.TEXT_PLAIN));
       } else record.setValue("email_body__c", "Email body exceeds maximum size allowed.");
       recordService.batchSaveRecords(VaultCollections.asList(record)).rollbackOnErrors().execute();
   }
}

Message Catalog

To provide a localized experience, Admins and Vault Java SDK developers can create translations for messages displayed to Vault end users. To do this, they either add messages with MDL or use the Message Catalog in Vault Help.

After creating messages, developers use TranslationService to retrieve the messages. Learn more in the Javadocs.

Admins coordinate message translation into any Vault supported language using the Bulk Translation tool in Vault Help.

Add Messages via MDL

Create the custom Messagegroup with at least one Message using MDL. For example, call the Execute MDL Script endpoint to add the message group and associated messages to Vault:

RECREATE Messagegroup person_validation__c (
   label('Person Validation Messages'),
   Message email_format__c(
      default_value('${email_input} is not a valid email address.')
   ),
   Message mobile_format__c(
      default_value('${mobile_input} is not a valid mobile number.')
   )
);

In addition to conforming to the standard MDL rules, message groups and messages have additional restrictions:

Once the messages are added to Vault, they can be called by code. However, for multiple languages to be supported, an Admin must translate the messages. For untranslated messages, Vault displays the default_value from the MDL, in Vault’s base language, regardless of a user’s preferred language.

Translate Messages

Translate messages using the Field Labels option of the Bulk Translation tool in the Vault UI. Learn more about localizing messages in Vault Help.

Messages can only be translated into Vault supported languages.

In the export CSV, messages have the message__sys Type. Their Key follows the pattern Messagegroup#Message. For example: person_validation__c#email_format__c.

Use Translated Messages in Java SDK Code

After the translated messages are imported into Vault, use TranslationService to add them to your custom code. Learn more in the Javadocs.

At runtime, users see messages in their preferred language. If no translation exists, Vault’s base language, the MDL default_value, is used.

Record Merges

Duplicate records in Vault can happen due to migrations, integrations, or day-to-day activities and may be difficult to resolve. Vault provides a solution to duplicate records by allowing you to merge two records together. You can only initiate record merges through the Vault API or the Vault Java SDK, and you cannot initiate a record merge through the Vault UI.

When merging two records together, you must select one record to be the main_record_id and one record to be the duplicate_record_id. The merging process updates all inbound references (including attachments) from other objects that point to the duplicate record and moves those over to the main record. Field values on the main record are not changed, and when the process is complete, the duplicate record is deleted.

The following example uses RecordService to execute two record merges within the account__v object:

 LogService logService = ServiceLocator.locate(LogService.class);
      RecordService recordService = ServiceLocator.locate(RecordService.class);
      RecordMergeSetInput recordMergeSetInput1 = recordService.newRecordMergeSetInputBuilder()
          .withDuplicateRecordId("V6G000000001005")
          .withMainRecordId("V6G000000001002")
          .build();


          RecordMergeSetInput recordMergeSetInput2 = recordService.newRecordMergeSetInputBuilder()
          .withDuplicateRecordId("V6G000000006002")
          .withMainRecordId("V6G000000001002")
          .build();


          RecordMergeRequest request = recordService.newRecordMergeRequestBuilder()
          .withObjectName("account__v")
          .withRecordMerges(VaultCollections.asList(recordMergeSetInput1, recordMergeSetInput2))
          .build();


          recordService.mergeRecords(request)
              .onSuccess(success -> {
                  String jobId = success.getJobId();
                  logService.info(String.format("Successfully started merge with job [%s]", jobId));
                  })
                  .onError(recordMergeOperationError -> {
                      recordMergeOperationError.getErrors().forEach(error -> {
                          String errorMessage = error.getError().getMessage();
                          RecordMergeOperationErrorType errorType = error.getErrorType();
                          if (errorType.equals(RecordMergeOperationErrorType.INVALID_DATA)) {
                              logService.info(String.format("Failed to start merge due to INVALID_DATA. Error: [%s]", errorMessage));
                          } else if (errorType.equals(RecordMergeOperationErrorType.OPERATION_NOT_ALLOWED)) {
                               logService.info(String.format("Failed to start merge due to OPERATION_NOT_ALLOWED. Error: [%s]", errorMessage));
                          }
                        });
                    })
                    .execute();

Learn more about executing record merges with the Vault Java SDK in the Javadocs.

Before you can execute a record merge, your Vault must be correctly configured. For example, the Vault object must have merges enabled and the initiating user must have permission to execute merges. Learn more about the configuration required for record merges in Vault Help.

Record Merge Limits & Restrictions

Record Merge Event Handlers

Your organization may want to create custom Record Merge Event Handlers which execute custom logic immediately when a record merge begins or after a record merge completes. You can initiate a record merge operation through the Vault API or the Vault Java SDK. Record Merge Event Handlers execute regardless of the merge operation origin, for example, starting a merge from the SDK or the API. The @RecordMergeEventHandlerInfo annotation indicates that a class is a Record Merge Event Handler and specifies the object where this handler will execute. Each Vault object can only have one (1) Record Merge Event Handler.

The RecordMergeEventHandler interface contains the logic for your record merges. Custom logic can execute either:

For example, your organization may need a record merge event handler if you have an external integration that relies on records that may be undergoing a merge. onMergeStart(), the handler can send a message to the integration that it should pause integration activities. onMergeComplete(), the handler can send another message that integration can resume again.

The following example is a skeleton for a Record Merge Event Handler:

@RecordMergeEventHandlerInfo(object = "account__v")
      public class RecordMergeEventHandlerSample implements RecordMergeEventHandler {


     @Override
     public void onMergeStart(RecordMergeEventStartContext context) {
         LogService logService = ServiceLocator.locate(LogService.class);


         String objectName = context.getObjectName();
         String jobId = context.getJobId();
         List mergeSets = context.getRecordMergeSets();
         for (RecordMergeSet mergeSet : mergeSets) {
             String dupRecordId = mergeSet.getDuplicateRecordId();
             String mainRecordId = mergeSet.getMainRecordId();
             logService.info(String.format("Started merge of duplicate record [%s] into main record [%s] for object [%s] job [%s]", dupRecordId, mainRecordId, objectName, jobId));
             //do something
         }
     }
     @Override
     public void onMergeComplete(RecordMergeEventCompletionContext context) {
         LogService logService = ServiceLocator.locate(LogService.class);


         String objectName = context.getObjectName();
         String jobId = context.getJobId();
         List mergeSetResults = context.getRecordMergeSetResults();
         for (RecordMergeSetResult mergeSetResult : mergeSetResults) {
             String dupRecordId = mergeSetResult.getDuplicateRecordId();
             String mainRecordId = mergeSetResult.getMainRecordId();
             RecordMergeCompletionStatus status = mergeSetResult.getStatus();
             if (status == RecordMergeCompletionStatus.SUCCESS) {
                 logService.info(String.format("Succeeded in merge of duplicate record [%s] into main record [%s] for object [%s] job [%s]",  dupRecordId, mainRecordId, objectName, jobId));
                 //do something for success
             } else if (status == RecordMergeCompletionStatus.FAILURE) {
                 logService.info(String.format("Failed in merge of duplicate record [%s] into main record [%s] for object %s job %s",  dupRecordId, mainRecordId, objectName, jobId));
                 //do something for failure
             }
         }
     }
 }

After writing your handler code, you must deploy your Record Merge Event Handler to make it active in your Vault. You can view all of the extensions currently deployed to your Vault in Admin > Configuration > Vault Java SDK.

Learn more about Record Merge Event Handlers in the Javadocs.

SDK Integrations

With the Vault Java SDK, you can build custom Vault integrations to automate business processes across different Vaults or with an external system. Spark Messaging allows your Vault to send messages from a Vault extension, and HTTP callout allows you to callback for any data you need. These operations perform asynchronously, allowing performant and seamless integration.

For example, a new change control record in Vault QMS causes a trigger to fire which sends a Spark message to Vault Registrations. This message starts a set of regulatory activities in Vault Registrations, and HTTP callout calls back to Vault QMS for any necessary information about the change control.

See our sample code for more examples of Vault Integrations.

In the diagram above, a trigger or action in Vault A calls QueueService which, through a Local Connection, adds a message into an Inbound Spark Message Queue. At the same time, without needing a connection, QueueService also adds messages into an Outbound Spark Message Queue.

The Inbound Queue sends the message to the attached Spark Message Processor to handle the message. This a local integration where a Spark message can perform operations within the current Vault.

The Outbound Queue sends messages to two places. One message goes to Vault B through a Vault to Vault Connection where it received by an inbound queue. The inbound queue then processes this message with its attached Spark Message Processor. This is a Vault to Vault integration where a message from one Vault can “spark” actions in a completely different Vault.

The other message sent by the outbound queue goes through an External Connection to an External Application. The external application must be prepared to handle the incoming message. This is an external integration where Vault can communicate with a completely separate application.

After receiving Spark messages, any of these applications (whether a Vault or external application) can call the Vault REST API to gather more information. For example, the Spark message processor in Vault B may use HTTP Callout to request additional information from Vaut A while processing the message. Learn more about Spark and HTTP Callout below.

Custom SDK code cannot reference standard, system-managed Vault to Vault connections. This helps avoid security vulnerabilities between functional teams. Learn more about standard Vault connections in Vault Help.

Spark Messaging

Spark Messaging is a message notification system that allows for loosely coupled, asynchronous, near real-time integration between Vaults or external applications. Spark messages are lightweight, signed messages designed to quickly send small amounts of data. If you need more information in your request, it’s expected you will use HTTP Callout to request additional information.

Messages are processed by a queuing system to provide reliable delivery. Outbound messages are placed in an outbound queue, and delivered messages enter an inbound queue. Once delivered, the inbound messages received from another Vault are processed by a Spark message processor.

For example, you may need to replicate digital assets from a PromoMats Vault to a publishing website. With Spark messaging, you can notify the publishing website when digital asset documents are approved. This in turn allows the publishing website to call Vault to retrieve details of the digital asset and format it for publishing. Without Spark Messaging capability, it would be necessary for the publishing website to periodically check for any newly created digital assets.

See our sample code for more examples of Spark Messaging.

Before you can send Spark messages, you must set up your Vault.

Message Format

Spark messages are always in a consistent format. The following is an example of a Spark message:


POST / HTTPS/1.1
Content-Type: application/json
Host: regionxyz.its.proxy.veevavault.com
Connection: Keep-Alive
User-Agent: Veeva Vault Spark Message Agent
X-VaultAPISignature-CertificateId: 00001
X-VaultAPISignature-ExecutionId: a123bede-32cb-4dbc-a7d9
X-VaultAPISignature-RequestId: ffjkek809809fjklkfjlkjf89
X-VaultAPISignature-RequestDateTime: 2012-04-25T21:49:27.719Z
X-VaultAPISignature-RequestNotBefore: 2012-04-25T21:48:27.719Z
X-VaultAPISignature-RequestNotAfter: 2012-04-25T21:54:27.719Z
X-VaultAPISignature-RequestType: spark_message
X-VaultAPISignature-URL: https//www.etech.com/Fservices/vaultmessage
X-VaultAPISignature-VaultId: 1000023
X-VaultAPI-Signature: DMXvB8r9R83tGoNn0ecwd5UjllzsvSvbItzfaMpN2nk5HVSw7XnOn49IkxDKz8YrlH2qJXj2iZB0Zo2O71c4qQk1fMUDi3LGpij7RCW7AW9vYYsSqIKRnFS94ilu7
X-VaultAPI-SignatureV2: ZEn78/hRc5Xip9/S2zAeJoaeD0ZkC1go3hOABF3dW/+/P+ATEZZiq/8L39ohTj052e+/F35ggteJtiXpddyStRrA9fYkDuAMDcQLhJ6tTIlHmL6jz6/0hjqavm5VbeGLAxcTqnSDDtKmXN4uBPsuV83reVekeDDkeRVZ2MkbPR7rkXNLt4AA1DhLS4vstwpAhHapuIWyt06npuB6HNfZL

{
  "vault_name" : "Megatech RIM Vault",
  "vault_host_name" : "biorad-rim.veevavault.com",
  "queue_name" : "study_sync_with_med_innovation__c",
  "enter_queue_timestamp" : "2012-04-25T21:49:25.719Z",
  "send_message_timestamp" : "2012-04-25T21:49:27.719Z",
  "send_attempt" : 2,
  "message_id" : "bb28d4ca-3a37-4fef-91ae-93c3a4ec1d8d",
  "message" : {
               "attributes": {
                              "object" : "product_brand__c",
                              "has_related" : true,
                              "related_count" : 100,
                              "authorization" :
                                  "A109315AC45D0FA76A5891FE25B2FCBB1AEBDBDDF
                                   25008682BEC50BF43F5DD9A96700A962515703060
                                   53E4571108799F7141A1857A571786AEF5A626655
                                   7B380"
                             },
               "items": [
                          "OP0000000010I13",
                          "OP0000000000I09",
                          "OP0000000022T06"
                        ]
               }
}

Headers

Note that message headers may show in a different case depending on the receiving host, so developers should be prepared to handle the headers as case-insensitive.

Name Description
Host Vault’s outbound proxy host name.
User-Agent The client software originating the request. For Spark messaging, this will always be Veeva Vault Spark Message Agent.
X-VaultAPISignature-CertificateId A certificate ID unique to this message. The application receiving this message must use this ID to retrieve the public key.
X-VaultAPISignature-ExecutionId A unique ID to identify the execution thread.
X-VaultAPISignature-RequestId A unique request ID, such as Message ID.
X-VaultAPISignature-RequestDateTime The time this message was sent.
X-VaultAPISignature-RequestNotBefore The time when this message first becomes valid. You should not attempt to verify a message before this time.
X-VaultAPISignature-RequestNotAfter The time when this message first becomes invalid. You should not attempt to verify a message after this time.
X-VaultAPISignature-RequestType The type of request, either spark_message or http_callout.
X-VaultAPISignature-URL The URL of the request from the QueueService.putMessage(Message) call. This is the intended recipient of the message.
X-VaultAPISignature-VaultId The Vault ID which sent this message.
X-VaultAPI-Signature For Vault version 20R1.0: The message signature, signed with Vault’s private key. You need this value to verify the message.
X-VaultAPI-SignatureV2 For Vault version 20R1.2+: The message signature, signed with Vault’s private key. You need this value to verify the message.

Body

The message body is a JSON object with the following attributes:

Name Description
vault_host_name A Vault’s DNS host name.
queue_name The name of the outbound queue the message came from.
enter_queue_timestamp The time in UTC this message entered the queue. Specifically, this is the exact time SDK code called QueueService.putMessage(Message).
send_message_timestamp The time in UTC this message was sent, or left the queue.
send_attempt The number of delivery attempts made for this message. Learn more about message delivery.
message_id The message ID.
message A JSON object which contains the sub-attributes attributesand items. View this object in the javadocs.
attributes A sub-attribute of the message JSON object which contains the attributes specified by Message#setAttribute(). SDK developers can add up to 10 custom attributes in a single message. Each attribute cannot exceed 100 characters. A common attribute is authorization, set with a token. You can also tie an integration_point__sys to a Spark message to create user exceptions.
items A sub-attribute of the message JSON object which contains a an array of Strings for each item added with QueueService. SDK developers can add up to 500 items in a single message. Each item cannot exceed 100 characters.

Message Delivery

If message delivery fails, Vault will immediately retry the delivery one (1) time. If this immediate retry fails, Vault retries at diminishing intervals using an exponential backoff algorithm for the next six (6) hours.

For example:

  1. First delivery of the message fails
  2. Immediately retry delivery
  3. After 1 second, retry delivery
  4. After 4 seconds, retry delivery
  5. After 7 seconds, retry delivery

And so on until the total time spent attempting to deliver the message is six (6) hours. Note that the time waited between retries will never exceed 15 minutes.

When the final delivery retry fails, Vault’s behavior is determined by the Spark Message Delivery Event Handler specified in the Spark Queue. To implement custom behavior for failed Spark messages, see Message Delivery Event Handler.

Message Signing & Verification

Spark Messages are signed using a private key to create a digital signature. External applications can then verify this signature with a public key.

To verify a Spark message, you need the following three things:

  1. Public Key
  2. Message Signature
  3. The String-to-verify

Retrieving the Public Key

  1. Retrieve the Vault certificate ID from the received message header, X-VaultAPISignature-CertificateId.
  2. Use this Certificate ID as input for a Vault REST API call to Retrieve the Signing Certificate. This call returns the public key as a .pem file.

GET /api/{version}/services/certificate/{X-VaultAPISignature-CertificateId}

Note that message headers such as X-VaultAPISignature-CertificateId may show in a different case depending on the receiving host, so developers should be prepared to handle the headers as case-insensitive.

Retrieving the Message Signature

For Vaults on version 20R1.0, the message signature is from the request header X-VaultAPI-Signature.

For Vaults on version 20R1.2+, the message signature is from the request header X-VaultAPI-SignatureV2.

Creating the String-to-Verify

The String-to-verify must be in the following format:

Verifying Message Signature

Once you have all three necessary pieces of information, you can use your digital signature library in your application platform to verify the signature. You can see an example of this in our code samples.

Message Delivery Allowlist

Outbound Spark messages are sent from an IP address associated with Veeva Vault. To accept Spark message delivery, modify your network and firewall rules by allowlisting *.veevavault.com.

Vault does not support authentication with the Vault REST API for Spark message delivery.

Vault Setup

Spark Messaging utilizes a queue system to handle messages in a First-In, First-Out manner. Each queue must also have a valid Connection. Before you can send or receive Spark messages, you must first set up this system in Vault.

Connections

A Connection holds configuration data necessary for integrations. For example, a Vault to Vault Connection contains the remote Vault ID, and an External Connection contains the external application’s URL.

Once a connection is established, you can add the Connection to a queue to send or receive Spark messages.

Learn more about setting up connections in Vault Help.

Spark Queues

To send Spark messages, you must create an outbound queue with a valid Queue Connection, meaning the queue is properly connected to the message destination. A queue can have multiple Connections, which means a single outbound queue can send messages to multiple destinations. For example, a Vault to Vault connection and an external connection can use the same outbound queue to send messages.

To receive Spark messages, you must create an inbound queue with a valid Queue Connection, meaning the queue is properly connected to the message source. A queue can have multiple Connections, which means a single inbound queue can receive messages from multiple sources. For example, a Vault to Vault connection and an external connection can send messages to the same inbound queue. Inbound queues also require a Spark Message Processor, which provides the logic to handle received messages.

Learn more about setting up queues in Vault Help.

Once queues are set up, you can manage your queues through the Vault UI or the REST API. For example, you can check the current status of a queue or disable queue delivery.

To troubleshoot your Spark queue configuration, you can view the Queue Logs in Admin > Logs > Queue Logs.

QueueService

To send a Spark message, you must first place the message into a Vault queue with QueueService. You must do this within a Vault extension. For example, a trigger or action can call QueueService to add a message to a queue.

You must deploy your Vault extension to make it available for configuration in your Vault. You can view all of the extensions currently deployed to your Vault in Admin > Configuration > Vault Java SDK.

Learn more about QueueService in the Javadocs.

MessageProcessor

To receive a Spark message in a Vault, you must create the logic to tell Vault what you intend to do with the information inside this message. You can create this logic inside a MessageProcessor. When creating an inbound queue to accept messages, you can select the Spark message processor to use with this inbound queue.

You must deploy your Spark message processor to your Vault to make it available for use with an inbound queue. You can view all of the processors currently deployed to your Vault in Admin > Connections > Spark Message Processors.

Learn more about MessageProcessor in the Javadocs.

Message Delivery Event Handler

Implementing a custom MessageDeliveryEventHandler can override the default behavior for failed Spark messages. When the final message delivery retry fails, Vault passes contextual and delivery-failure information to the handler, which you can use in your onError() logic to determine what actions to take. For example, you could put the message back on the queue in 24 hours, notify a specific user, create a user task record, or take other actions.

When creating an outbound queue to send messages, admins can select the Spark Message Delivery Event Handler, the class implementing MessageDeliveryEventHandler. If a handler is associated with a queue, a Spark Message Delivery Event Handler User is required.

By default, handler code executes with Vault Owner level access, identified in audit logs as Java SDK Service Account on behalf of {originatingUser}, where {originatingUser} is the Spark Message Delivery Event Handler User.

You must deploy your handler code to your Vault to make it available for use with an outbound queue. If the handler is not associated with an outbound queue, the deployed code will not be executed when a Spark message delivery failure occurs. You can view all of your Vault queues in Admin > Connections > Spark Message Delivery Event Handlers.

Learn more about MessageDeliveryEventHandler in the Javadocs.

Integration Rules

When using Spark messaging for Vault to Vault integrations, it may be necessary to transform data from the source Vault’s data model to fit within the target Vault’s data model. Integration Rules allow developers to incorporate configurable rules for mapping object and document fields between two Vaults into a MessageProcessor.

Vault only supports integration rules for Vault to Vault integrations.

Available transformation rules include:

Integration rules are fully configured and evaluated on the target Vault. This preserves the loosely coupled characteristic of Spark messaging integrations. It is the target Vault’s responsibility to process the received message.

To set up integration rules:

  1. Set up Integration rules through the Vault UI. Learn more about this process in Vault Help.
  2. Optional: To use reference lookups, you must set up reference lookup records to map data key values between Vaults. Learn how to set these up in the Vault UI in Vault Help.
  3. Develop a MessageProcessor with the following logic:
    1. Retrieve data from the source Vault through VQL and HTTP Callout.
    2. Set the VQL Describe Query header X-VaultAPI-DescribeQuery to true, which returns the source Vault data model.
    3. Use IntegrationRuleService to evaluate your integration rules.

Field Mapping

Field mapping rules allow you to set the value of a field on a target object or document directly from a value returned from a VQL query to the source Vault. This is useful when you need to derive field values for a record in the target Vault from data coming from the source Vault.

For example, you may want to provide the Name of a record from the source Vault as a cross-reference within an object field on the target Vault.

When you create an HTTP Callout for a field rule, you may need to add functions such as RICHTEXT() or TONAME() based on the query field type. You can retrieve the field type using getQueryFieldType.

The example below retrieves the type of an object field, then appends the appropriate VQL function to the query field before adding it to a list, which can be included in an HTTP Callout.

@MessageProcessorInfo()
public class ExampleProcessor implements MessageProcessor {
   public void execute(MessageContext context) {

      IntegrationRuleService service = ServiceLocator.locate(IntegrationRuleService.class);
      GetIntegrationRulesRequest request = service.newGetIntegrationRulesRequestBuilder().build();
      GetIntegrationRulesResponse response = service.getIntegrationRules(request);
      for (IntegrationRule integrationRule : response.getIntegrationRules()) {

         List<String> selectFields = VaultCollections.newList();
         for (FieldRule fieldRule : integrationRule.getFieldRules()) {
            ObjectFieldType objectFieldType  = fieldRule.getQueryFieldType(ObjectFieldType.class);

            switch (objectFieldType) {
               case LONGTEXT:
                  selectFields.add("LONGTEXT(" + fieldRule.getQueryField() + ")");
                  break;
               case RICHTEXT:
                  selectFields.add("RICHTEXT(" + fieldRule.getQueryField() + ")");
                  break;
               default:
                  selectFields.add(fieldRule.getQueryField());
            }
         }
      }
   }
}

Reference Lookups

Reference lookup rules allow you to set the value of a field on a target object or document indirectly from a value returned from a VQL query to the source Vault, using a lookup. This is useful in cases where a reference value differs between the source and target Vault.

For example, you can indicate that the Country value “US” in the source Vault is equivalent to “United States” in the target Vault.

Reference Lookups support the following data types, listed in the ReferenceLookupType Enum:

As a best practice, you should use global identifiers such as external_id__v to map data. You should only use reference lookups in cases where this is not possible.

Target Field Lookups

Rather than using predefined values for reference lookups, you can also create dynamic reference lookups with the target_field_lookup field. For example, Admins can configure a field rule for a child object that uses parent__cr.name__v for an object reference instead of using predefined values. This eliminates the need to maintain reference lookup values, and instead developers can code their integrations to dynamically resolve object references.

The target_field_lookup field is only available if:

Generic

If the data type in the source Vault does not match the data type in the target Vault, or if you need to map a data type not listed in the ReferenceLookupType Enum, Admins can manually map data types using the GENERIC option.

GENERIC supports any-to-any data type mapping except for multi-value fields. For example, Boolean (Yes/No) fields are supported, and multi-value picklists are not supported. As a best practice, only use GENERIC if your required data type is not available.

Static Field Defaults

Field default rules allow you to set the value of a field on the target object or document to a default value. This is useful in cases where you want all records created via the integration to have a default value for a field. For example, you may want the Description field to say “created by a Vault Integration.”

You can also use field default rules in combination with field mapping or reference lookup rules. For example, you can specify a fallback value in cases where the query to the source Vault does not return a value.

When creating field defaults with the MDL Fieldrule subcomponent, note the following requirements for the field_default attribute based on field type:

User Exception Messages

With Spark user exceptions, your Vault to Vault integrations can display custom exception messages to Business Admins in the Vault UI. This helps Business Admins track and resolve any errors that occur with your integrations.

For example, if your integration fails to process an incoming message, you can create custom SDK logic to create a User Exception Message (exception_message__sys) record to capture the failure and display information to a Business Admin. This record can contain information for how to troubleshoot, what action is necessary, who to contact if the failure persists, and so on.

User exception messages are tied to individual Integration Points (integration_point__sys). Because of this, you cannot leverage user exception messages without setting up integrations and integration points. Learn more in Vault Help.

Creating exceptions which appear in the Vault UI is as simple as creating User Exception records with RecordService.

Developing User Exception Messages

First, your Spark message format must include an attribute pointing to the corresponding integration_point__sys API name. This ties all of your user exceptions to the appropriate integration point. It doesn’t matter what you name this attribute, but we recommend a meaningful name, such as integration_point.

To add an attribute to a Spark message, call Message#setAttribute(). For example:

    QueueService queueService = ServiceLocator.locate(QueueService.class);
    Message outboundMessage = queueService.newMessage("outbound_queue__c");
    outboundMessage.setAttribute("integration_point", "integration_point_api_name");
    queueService.putMessage(outboundMessage);

Which will add the attribute to your Spark message:

"message":{
  "attributes":{
    "integration_point": "create_regulatory_event__v"
  }
}

Next, add your user exception logic inside of a MessageProcessor. This logic must create a exception_message__sys record, and can optionally create exception_item__sys child records for each item in the exception. For example:

@MessageProcessorInfo()
  public class ProcessStudy implements MessageProcessor {
    public void execute(MessageContext messageContext) {
      Message incomingMessage = messageContext.getMessage();
      String integrationApiName = "integration_api_name";
      String integrationPointApiName = incomingMessage.getAttribute("integration_point", MessageAttributeValueType.STRING);
      createUserExceptionMessage(integrationApiName, integrationPointApiName);
    }

    private void createUserExceptionMessage(String integrationApiName, String integrationPointApiName) {
      RecordService recordService = ServiceLocator.locate(RecordService.class);
      String integrationId = getIntegrationId(integrationApiName);
      String integrationPointId = getIntegrationPointId(integrationPointApiName);
      List<Record> recordsToSave = getUserExceptionMessageRecord(integrationPointApiName, integrationId, integrationPointId);
      recordService.batchSaveRecords(recordsToSave)
        .rollbackOnErrors()
        .execute();
    }

    private List<Record> getUserExceptionMessageRecord(String integrationPointApiName, String integrationId, String integrationPointId) {
      RecordService recordService = ServiceLocator.locate(RecordService.class);
      Record userExceptionMessage = recordService.newRecord("exception_message__sys");
      List<String> errorTypePicklistValues = VaultCollections.newList();
      errorTypePicklistValues.add("message_processing_error__sys");
      userExceptionMessage.setValue("integration__sys", integrationId);
      userExceptionMessage.setValue("integration_point__sys", integrationPointId);
      userExceptionMessage.setValue("error_type__sys", errorTypePicklistValues);
      userExceptionMessage.setValue("error_message__sys", "Error message from IntegrationRuleService or RecordService");
      userExceptionMessage.setValue("name__v", integrationPointApiName);
      List<Record> recordsToSave = VaultCollections.newList();
      recordsToSave.add(userExceptionMessage);
      return recordsToSave;
    }
}

HTTP Callout

HTTP Callout allows custom Vault Java SDK code to make HTTP requests to other Vaults, to the Vault REST API, and even third-party systems. Because Spark Messages are lightweight, it is likely that messages will need to make HTTP callouts for additional data.

HTTP Callout allows you to automate business processes between Vaults. For example, a new change control record in Vault QMS causes a trigger to fire which sends a Spark message to Vault Registrations. This message starts a set of regulatory activities in Vault Registrations, but it needs more information. With HTTP Callout, Vault Registrations calls back to Vault QMS for the necessary information about the change control.

See our sample code for more examples of HTTP callout.

In the diagram above, a trigger or action in Vault A calls HttpService to make three HTTP Callout requests.

The first HTTP request is sent to Vault B through a Vault to Vault Connection. Once in Vault B, it requests information from the Vault API through Vault B’s Authorized Connection User.

The second request goes through a Local Connection and calls the Vault REST API with Vault A’s Authorized Connection User.

The third request is sent to an through an External Connection to an External Application. This application must provide resources for the request to obtain information.

HTTPService

HTTP Callout provides a Vault Java SDK service, HTTPService, to construct request and response objects and send requests. Requests are sent synchronously and send calls are blocking, meaning requests wait for a response until they timeout. HTTP requests happen outside of an SDK code transaction, so requests do not rollback.

HTTP request bodies are available in JSON or CSV format, through the use of JsonService and CsvService. Learn more in the Javadocs.

Best Practices

When creating HTTP Callouts, keep the following best practices in mind:

Vault Setup

To make an HTTP callout, you must first set up a Connection in Vault Admin. Connections can be set up for the local Vault, with another Vault, or with an external application. Local and Vault to Vault connection types can be set up with an Authorized Connection User to control what data access is allowed. External connection types may also be set up to make requests with basic authentication using username and password tokens.

Learn more about connections in Vault Help.

Tokens

The following types of tokens are available for Vault integrations: Authorization tokens, Session tokens, Vault tokens, and Custom tokens.

Authorization Tokens

For external connections, you can authenticate with the external application using Authorization tokens. For example, you can use Authorization tokens in an HTTP Callout request to the external application. The user name and password in a connection’s authentication record can be included in the request by using the following tokens:

Session Tokens

Session tokens can be included in a Spark message or HTTP Callout request to allow external applications to call the Vault REST API with an authenticated session. For calls to Vault API from an external application, you can make your HTTP requests with a Session token in the Authorization header:

Vault Tokens

All Vaults include system-provided tokens for Vault information: vault_id__sys, vault_name__sys, and vault_dns__sys. You can define and store up to ten (10) additional tokens using the Vaulttoken MDL component type. Once configured, Vault tokens can be used anywhere in your Vault.

For example, you might create a Vault token with the name my_subdomain__c and the value “veepharm” in your production Vault. In your sandbox Vault, you create another Vault token with the same name and the value “veepharm-sbx”. You can then set the URL to http://${Vault.my_subdomain__c}.veepharm.com in a Connection record, and Vault resolves the token to the correct subdomain for each Vault.

The following example MDL command creates a my_subdomain__c Vault token with a value of “veepharm-sbx”:

RECREATE Vaulttoken my_subdomain__c (
   label('Veepharm Sandbox'),
   active(true),
   clone_behavior('clear__sys'),
   type('string__sys'),
   system_managed(),
   value('veepharm-sbx')
   );

Custom Tokens

Custom tokens are defined using TokenService and are resolved at runtime. For example, you might create custom logic in a user-defined service to calculate batch size, then pass the batch size value to a ${Custom.batch_size__c} token. Learn more about token service.

Vault QMS

Some Vault extension interfaces are only available in QMS Vaults.

Quality External Notifications

In QMS Vaults, Quality External Notifications provide a way to send emails and share documents with users outside of your Vault. When an object has external notifications enabled, Admins can add the Create Distribution Groups Membership action to the object lifecycle to automatically populate distribution groups based on matching fields. In addition to the configuration options provided in the Vault Admin UI, developers can create custom logic to populate distribution group membership using the Vault Java SDK.

Learn more about configuring External Notifications in Vault Help.

Creating Custom Actions

A Create Distribution Group Membership action is a record action that uses interfaces from the externalnotification package, which is only available to QMS Vaults.

The extenalnotification package includes:

The following example shows a record action that uses ExternalNotificationMembership to retrieve the current distribution groups and their membership. It then uses ExternalNotificationMembershipService to add two new members and remove a third from the distribution group.

@RecordActionInfo(
    label="Update Existing Notification Membership",
    icon="generate__sys",
    usages={Usage.USER_ACTION}
)
public class UpdateExternalNotificationMembershipAction implements RecordAction {

    @Override
    public boolean isExecutable(RecordActionContext recordActionContext) {
        return true;
    }

    @Override
    public void execute(RecordActionContext recordActionContext) {
        for (Record record : recordActionContext.getRecords()) {
            String objectName = record.getObjectName();
            String recordId = record.getValue("id", ValueType.STRING);

            ExternalNotificationGroupMembershipService externalNotificationGroupMembershipService =
                ServiceLocator.locate(ExternalNotificationGroupMembershipService.class);

            externalNotificationGroupMembershipService.getExternalNotificationMembership(objectName, recordId)
                .onError(membershipReadError -> {
                    throw new RollbackException(
                        membershipReadError.getErrorType().toString(),
                        membershipReadError.getMessage()
                    );
                })
                .onSuccess(externalNotificationMembership -> {
                    Map<String, QualityDistributionGroupMembership> distributionGroupMembershipsByName =
                        externalNotificationMembership.getDistributionGroupMembershipsByName();

                    // Add Person 1 and Person 2, remove Person 3
                    QualityDistributionGroupMembership distributionGroupMembership1 =
                        distributionGroupMembershipsByName.get("dist_1__c");
                    if (distributionGroupMembership1 != null) {
                        Set<String> personIds = VaultCollections.newSet();
                        personIds.addAll(distributionGroupMembership1.getRecipients());
                        personIds.add("V0I000000001001"); // Person 1
                        personIds.add("V0I000000003004"); // Person 2
                        personIds.remove("V0I000000001004"); //Person 3
                        distributionGroupMembership1.setRecipients(personIds);
                    }


                    ExternalNotificationMembershipUpdateRequest membershipUpdateRequest =
                        externalNotificationGroupMembershipService.newExternalNotificationMembershipUpdateRequestBuilder()
                            .withDistributionGroupMemberships(distributionGroupMembershipsByName.values())
                            .build();

                    externalNotificationGroupMembershipService.updateExternalNotificationMembership(membershipUpdateRequest)

                        .rollbackOnErrors()
                        .execute();
                })
                .execute();
        }
    }
}

In most cases, you should include the logic to update distribution group membership within the execute() method of a record action. In cases where you want to reuse the same logic across multiple record actions, you can create a user-defined service with your distribution group logic. For example, you could call the same UDS in a user action and a user bulk action

Once you have deployed your custom Create Distribution Group Membership action, it will be available to add to supported object lifecycles in the Vault Admin UI.

QualityOne QMS

Built from Vault’s Email Processor feature, Admins can define how Vault automatically creates COA Inspection records from emails sent to an Inbound Email Address in the Vault Admin UI by using a QualityOne-provisioned Email Processor: the Supplier Initiated COA Email Processor.

Supplier Initiated COA Email Processor

In QualityOne QMS Vaults, the Supplier Initiated COA Email Processor allows your organization to send emails, containing your external parties COA file attachments, to Vault for document upload and record creation using an SDK entry point implementation. After creating a migration package from the implementation, Admins can import and validate the migration package to Vault, which enables the implementation under QualityOne COA Email Intake Handlers. You can customize the logic in the implementation to process information extracted from incoming emails and define COA Inspection records with the extracted information.

Learn more about setting up Automated COA Email Intake in Vault Help.

Understanding the SDK Entry Point Implementation

The QualityOne COA Email Intake Handlers implementation uses interfaces and annotation from the coaemailintake package available only for QualityOne QMS Vaults. The coaemailintake package includes the following:

Processing Email Information for COA Email Intake

The following example presents how the implementation handles incoming emails received by the Vault Email Service to the inbound email address and processes the information extracted. The QualityOneCoaEmailIntakeHandlerInfo annotation defines accepted emails when the “To” field matches the Admin-specified inbound email address named cholecap_coa_supplier_inbound__c.

@QualityOneCoaEmailIntakeHandlerInfo(inboundEmailAddress = "cholecap_coa_supplier_inbound__c")

After executing the QualityOneCoaEmailIntakeHandler, there’s a response to retrieve the list of email attachments and to create an empty list for excluded email attachments. The logic then excludes any attachments that are less than 1 megabyte in file size.

@QualityOneCoaEmailIntakeHandlerInfo(inboundEmailAddress = "cholecap_coa_supplier_inbound__c")
public class EmailIntakeHandler implements QualityOneCoaEmailIntakeHandler {

  @Override
  public QualityOneCoaEmailIntakeHandlerResponse execute(QualityOneCoaEmailIntakeHandlerContext context) {
    List<QualityOneEmailAttachment> emailAttachments = context.getEmailAttachments();
    List<QualityOneEmailAttachment> excludedAttachments = VaultCollections.newList();

    for (QualityOneEmailAttachment emailAttachment : emailAttachments) {
      if (emailAttachment.getFileSize() < (long) 1024 * 1024) {
        excludedAttachments.add(emailAttachment);
      }
    }
  }
}

Then the implementation extracts the Inspection Plan’s ID value from the email subject line and builds a QualityOneIntakeInspection object containing the Inspection Plan value and the included email attachments.

String inspectionPlan = context.getEmailSubject();

QualityOneIntakeInspection intakeInspection = context.newIntakeInspectionBuilder()
  .withValue("inspection_plan__v", inspectionPlan)
  .build();

return context.newCoaEmailIntakeProcessorResponseBuilder()
  .withIntakeInspection(intakeInspection)
  .withExcludedAttachments(excludedAttachments)
  .build();

Once the information has been processed, the implementation sends the QualityOneIntakeInspection object to Vault, triggering the Supplier Initiated COA Email Intake email processor to create new COA Inspection records and upload the email attachments as documents based on the information extracted from the QualityOneIntakeInspection object.

@QualityOneCoaEmailIntakeHandlerInfo(inboundEmailAddress = "cholecap_coa_supplier_inbound__c")
public class EmailIntakeHandler implements QualityOneCoaEmailIntakeHandler {

  @Override
  public QualityOneCoaEmailIntakeHandlerResponse execute(QualityOneCoaEmailIntakeHandlerContext context) {
    List<QualityOneEmailAttachment> emailAttachments = context.getEmailAttachments();
    List<QualityOneEmailAttachment> excludedAttachments = VaultCollections.newList();

    for (QualityOneEmailAttachment emailAttachment : emailAttachments) {
      if (emailAttachment.getFileSize() < (long) 1024 * 1024) {
        excludedAttachments.add(emailAttachment);
      }
    }

    String inspectionPlan = context.getEmailSubject();

    QualityOneIntakeInspection intakeInspection = context.newIntakeInspectionBuilder()
      .withValue("inspection_plan__v", inspectionPlan)
      .build();

    return context.newCoaEmailIntakeProcessorResponseBuilder()
      .withIntakeInspection(intakeInspection)
      .withExcludedAttachments(excludedAttachments)
      .build();
  }
}

Customizing Implementation Logic

When customizing the logic for your Supplier Initiated COA Email Processor, ensure you include the logic to extract either the Inspection Plan, Purchase Order, Purchase Order Line Item, or Material value from the subject line. Extracting either of these values helps to relate the Inspection Plan to the newly-created COA Inspection record when the Supplier Initiated COA Email Intake email processor triggers. You can customize the logic to not exclude any files from an incoming email or to exclude specific files based on your organization’s criteria. You can customize the logic in the QualityOneCoaEmailIntakeHandler interface for specific ways to handle errors. For example, you may want to set a specific COA Ingestion Status and COA Ingestion Error Type value when the interface fails to identify the subject line value or when errors occur when running the QualityOneIntakeInspection interface.

Limits & Restrictions

While developing Vault extensions is essentially programming in Java, there are some limits and restrictions to ensure your code runs securely in Vault.

Limits

Vault enforces limits at runtime to protect against excessive uses that may impact overall performance. Vault tracks custom code execution and terminates any code execution that reaches a limit. Vault then rolls back the transaction and presents a runtime error to the user to contact an Admin for assistance.

Vault does not enforce limits during debugging. However, Vault tracks and enforces service calls that execute on the server, such as QueryService and RecordService. If the service calls cause a limit violation error, the custom code will fail when you deploy them to Vault.

Time Limits

Time limits apply to an entire request, including all BEFORE, AFTER, and nested triggers in a single request transaction. If a transaction fails due to a time limit violation, you can find more information about the violation in the debug log.

Size Limits

If a transaction fails due to a size limit violation, you can find more information about the violation in the debug log.

Memory Limits

Maximum memory allocated for a top-level Vault extension is 40MB. Nested extensions must work within this limit, but their execution amounts are deallocated upon completion.

For example:

  1. A user initiates a record action. This action has 40MB total, and uses 10MB.
  2. The record action indirectly initiates a record trigger. This trigger has 30MB to work with, and uses 5MB.
  3. The record trigger initiates a nested trigger. This nested trigger has 25MB, and uses 25MB.
  4. The nested trigger finishes execution. The original record trigger still has 25MB, because completed execution no longer counts towards this limit.
  5. The original record trigger finishes execution. The record action now has 30MB to work with, because the completed record trigger does not count towards this limit.
  6. The record action uses an additional 10MB before finishing. The total memory for this action is 20MB, with 20MB remaining.

Developers can help manage memory usage by creating a User-Defined Service. Return values and method parameters used by the service count toward the 40MB limit, but all other memory that is consumed inside the service will be freed when method execution is complete. Vault enforces a 400MB gross memory limit for the entire life of the transaction.

If any transaction fails due to memory limits, the entire extension fails. For example, if the nested trigger in step 3 used more than 25 MB, the entire transaction rolls back and the end-user will see an error message. Learn more about troubleshooting runtime errors.

Restrictions

Vault checks restrictions when a user uploads code to prevent unsafe use of Java. Validation occurs when deploying the uploaded source code to Vault. If there are any validation errors on deployment, the deployment fails and Vault sends an error message to the debug log.

The following are some examples of restrictions:

It’s important to keep restrictions in mind during development, especially since Vault does not enforce these restrictions while debugging code. Restrictions are only checked when uploading your code to Vault.

Method Name Restrictions

You cannot use any of the following method names:

Any reference to a method with a restricted name will fail when uploaded to your Vault.

Exception Handling Restrictions

Only RollbackException can be thrown and caught. Attempting to catch any other exception results in an error. You must use RollbackException to modify the error message shown to users. Note the exception cannot be stopped/swallowed.

Debugger Restrictions

Only users with the Vault Owner security profile can attach a debug session to a Vault. Learn more about managing security profiles in Vault Help.

Only Vaults in the Sandbox domain can have debug sessions attached.

Limits:

JDK Allowlist

You may only use allowlisted JDK classes and interfaces in your Vault extensions. All other libraries in the JDK are not allowed.

The following Vault permissions control actions for deploying and managing code in Vault. Learn more about permission sets in Vault Help.

Permission Label Controls
Vault Owner security profile You must have the standard Vault Owner security profile to connect to the Vault Java SDK Debugger.
Admin: Vault Java SDK: Read Ability to read Vault Java SDK code; you’ll need this permission to deploy code, validate code, export a VPK which contains code, or download source code.
Admin: Vault Java SDK: Create Ability to create Vault Java SDK code; you’ll need this permission to deploy code, update existing code, or enable and disable extensions.
Admin: Vault Java SDK: Edit Ability to create Vault Java SDK code; you’ll need this permission to deploy code, update existing code, or enable and disable extensions.
Admin: Vault Java SDK: Delete Ability to delete Vault Java SDK code; you’ll need this permission to deploy code or delete existing source code.
Admin: Migration Packages: Deploy Ability to deploy packages; you’ll need this permission on the target Vault to deploy code.
Objects: Inbound Package: Read Ability to view the Inbound Package object. You must also have Read permission on all fields for this object.
Objects: Inbound Package Step: Read Ability to view the Inbound Package Step object. You must also have Read permission on all fields for this object.
Objects: Inbound Package Data: Read Ability to view the Inbound Package Data object. You must also have Read permission on all fields for this object.
Objects: Inbound Package Component: Read Ability to view the Inbound Package Component object. You must also have Read permission on all fields for this object.

Data Type Map

When working with Vault field values in the Vault Java SDK, you must map the data type configured in a Vault field to a Java data type in order to manipulate the field value in Java code.

You can learn more about Vault object and document fields in Vault Help.

The com.veeva.vault.sdk.api.core.ValueType interface also provides this mapping.

Vault Field Type ValueType Returned Data Type
Text ValueType.STRING String
Yes/No VauleType.BOOLEAN Boolean
Number ValueType.NUMBER BigDecimal
Date ValueType.DATE LocalDate
DateTime ValueType.DATETIME ZonedDateTime
Picklist ValueType.PICKLIST_VALUES List<String>
Object ValueType.STRING String
Parent ValueType.STRING String
Lookup Same as Source Depends on ValueType
ID ValueType.STRING String
Multi-Value References (Documents only) ValueType.REFERENCES List<String>
Currency ValueType.NUMBER BigDecimal
LongText ValueType.STRING String
RichText ValueType.STRING String

Troubleshooting Runtime Errors

Typically, a developer can discover and fix most errors by debugging and testing code during development. However, in some cases, errors can occur at runtime causing custom code execution to terminate. When these errors occur, a developer needs to investigate the cause and fix the code accordingly.

Generally, there are three types of runtime errors:

All three types of errors cause:

As shown in the message above, Vault directs end-users to an Admin for assistance. The caused by message detail helps developers identify the cause of the error. In some cases, the error message is all a developer needs to fix the error. If a developer needs more information, they can check the Debug Log to further troubleshoot the issue.

Debug Log

From the Logs area in Vault (Admin > Logs > Vault Java SDK Logs), you can view the Debug Log. The debug log captures custom Vault Java SDK code execution details as well as standard messages from Vault. Every request initiated by a user for which logging is enabled generates a log file. You can apply filters to narrow log entries to only include certain error types (log levels) or classes.

By default, the Vault Owner and System Admin security profiles have permission to view the debug log and set up debug log sessions for a particular user. Note that you can only set up debug log sessions for 20 users per Vault. Learn more about Debug Logs in Vault Help.

The log file may contain the following error types:

The log file captures the following information:

For example, a trigger error in the debug log may look like this:

2017-11-29 05:57:39,992 Recordtrigger.com.veeva.vault.custom.CreateRecords INFO *****Start Execution:[com.veeva.vault.custom.triggers.CreateRecords]*****
2017-11-29 05:57:39,994 Recordtrigger.com.veeva.vault.custom.CreateRecords INFO *****End Execution:[com.veeva.vault.custom.triggers.CreateRecords]*****
2017-11-29 05:57:39,995 Recordtrigger.com.veeva.vault.custom.triggers.CreateRecords INFO CPU(ns)=42204304 elapsed(ms)=335 memory(b)=782528
2017-11-29 05:57:39,997 Recordtrigger.com.veeva.vault.custom.CreateRecords ERROR ErrorId[d8881313-71f7-429f-a81b-2a700496fd6c] java.lang.Throwable: Vault Java SDK Error: [Encountered [2] error(s) in batch.] Error Id: [d8881313-71f7-429f-a81b-2a700496fd6c]
    at ...(Unknown Source)
Caused by: java.lang.Throwable: [Input Position [0] Error [{type='PARAMETER_REQUIRED', subtype='null', message='Missing required parameter [abbreviation__c]'}], Input Position [1] Error [{type='PARAMETER_REQUIRED', subtype='null', message='Missing required parameter [abbreviation__c]'}]]

Service Calls

For service calls only, the debug log also contains granular information about how long it took each call to execute.

Parameter Description
count The number of times this method executed. For example, count=3 means this method executed three (3) times.
elapsed The amount of time in milliseconds for all counts to complete execution. For example, if count=3, the elapsed time is the amount of time it took all three executions to complete.
avg The average amount of time in milliseconds it took each count to complete. For example, if count=3 and elapsed=3ms, it took each count 1ms to complete on average. This value is floored. For example, if the average is 1.9ms per count, this average rounds down to 1ms. If the average time is less than 1ms, this average rounds down to 0ms.

For example, a call to RecordService in the debug log may look like this:

2017-11-29 05:57:39,995 Recordtrigger.com.veeva.vault.custom.triggers.CreateRecords INFO com.veeva.vault.sdk.api.data.RecordService#newRecord(java.lang.String) - [count=3, elapsed=3ms, avg=1ms]

HTTP Callout

If your Vault extension uses HTTP Callout to make an HTTP request, there is additional information about each request in the debug log.

Parameter Description
HttpRequest The HTTP method and the URL for this request.
HttpResponse The HTTP response code for this request.

For example, an HTTP request in the debug log may look like this:

2017-11-29 05:57:39,281 Recordtrigger.com.veeva.vault.custom.triggers.CreateRecords INFO HttpRequest:[POST:https://myvault.veevavault.com/api/v18.3/vobjects/product__v]
2017-11-29 05:57:39,313 Recordtrigger.com.veeva.vault.custom.triggers.CreateRecords INFO HttpResponse:[200]

Example Troubleshooting Flow

  1. A user encounters an error while using Vault Java SDK.
  2. A Vault Owner or System Admin sets up a debug log session for a user from Admin > Logs > Debug Logs.
  3. The user reproduces the issue during the session.
  4. The debug log captures all necessary information, allowing the Vault Owner or System Admin to determine the source of the issue.

Adding Custom Debug Log Messages

The LogService allows developers to send a message directly to the Debug Log to help troubleshoot issues. This is especially helpful when troubleshooting an issue that only occurs at runtime, meaning it’s not reproducible in debugging. For example, a developer can write a variable value to the debug log at runtime.

Refer to the Javadocs for details about using this service.

Runtime Log

Runtime Logs capture additional logging for Vault Java SDK transactions. Unlike the Debug Log, which is enabled per user, the Runtime Log is enabled per Vault. Because the runtime logs are always running, these logs can provide more consistent and reliable logging over the debug log. However, logging more than runtime exceptions may cause Vault performance degradation. Runtime logs capture data in 5-minute intervals. Daily logs are available for download in the Vault UI (Admin > Logs > Vault Java SDK Logs) and the REST API. Daily logs are stored for 30 days.

The content captured in the Runtime Logs depends on settings set by an Admin in the Vault UI under Settings > General Settings:

Runtime log entries are captured 15 minutes after the Vault Java SDK transaction completes. If you’ve recently encountered an error which is not captured in the runtime log, wait for the transaction to finish and check again.

Runtime logs capture data in 5-minute intervals with the following limits:

If a limit is reached, the Runtime log captures a LIMIT entry with the error details and stops capturing data. After the current 5-minute period elapses, logging resumes automatically.

Adding Custom Runtime Log Messages

The LogService allows developers to send a message directly to the Runtime Log to help troubleshoot issues. This is especially helpful when troubleshooting an issue that only occurs at runtime, meaning it’s not reproducible in debugging. For example, a developer can write a variable value to the runtime log at runtime.

Refer to the Javadocs for details about using this service.

Spark Queue Log

From the Logs area in Vault (Admin > Logs), you can view the Queue Logs. You must have the Configuration: Queues: Queue Log permission to view this log. This log only contains information about Spark queues.

By default, the queue logs only capture undelivered Spark messages. For example, when a message in a queue fails to deliver after the maximum number of retry attempts. Once a message is written to the queue log, Vault removes this message from the queue.

To capture information about all Spark message delivery, you must set the Trace Queue Delivery through the Vault UI. This captures delivery information for the selected queues for 20 minutes.

Vault produces a daily log for each of the previous 30 days. After 30 days elapse, the logs begin to delete, starting from the oldest log.

Log Contents

The queue log captures the following information:

Column Description
TimeStamp The time this message entered the log, in the format YYYY-MM-DDTHH:MM:SSZ. For example, 7:00am on January 15, 2016 would use the format 2016-01-15T07:00:00Z.
VaultId The ID of the Vault where this queue is located.
QueueName The name of the queue, such as outbound_queue__c.
ConnectionName The ID of this queue’s Connection object, such as V17000000000101.
MessageId The unique message ID, which is defined as X-VaultAPISignature-RequestId in the request header.
EntryType The type of queue log entry. Values are either REMOVAL for undelivered messages removed from the queue, or TRACE for messages added through Trace Queue Delivery.
QueueTime The time this message entered the queue, in the format YYYY-MM-DDTHH:MM:SSZ. For example, 7:00am on January 15, 2016 would use the format 2016-01-15T07:00:00Z.
SendTime The last time this queue attempted to send this message, in the format YYYY-MM-DDTHH:MM:SSZ. For example, if this queue last attempted to send the message at 7:00am on January 15, 2016, the log would show 2016-01-15T07:00:00Z, even if the queue made attempts previous to this time.
Attempt The number of times this queue attempted to deliver this message. If the queue never attempted to send the message, this column is blank.
StatusMessage The status message received for this message. For example, SUCCESS or Exception occurred: with an error message.
ResponseTime The time this queue received the response for this message, in the format YYYY-MM-DDTHH:MM:SSZ. For example, 7:00am on January 15, 2016 would use the format 2016-01-15T07:00:00Z.

Audit Logs

From the Admin > Logs area in Vault, you can view a history of actions within your Vault, including actions performed with the Vault Java SDK. You can learn more about the Vault Admin Logs in Vault Help.

System Audit History

The System Audit History page displays Vault-level configuration and settings changes, which includes managing Vault extensions. For example, uploading a new trigger to your Vault.

Object Record Audit History

Records affected by triggers indirectly through the use of the RecordService have an audit entry identifying the change with System on behalf of the user initiating the request. This allows an Admin to audit changes which may not have been directly manipulated by users, but rather by code in triggers. When a user delegates access to another user, the audit will show Java SDK Service Account on behalf of the delegating user.

In addition, when a workflow starts indirectly by code, the audit log will also indicate Java SDK Service Account on behalf of the user initiating a request. This is important because a user may create a new record that fires a trigger to start a workflow. In this case, the end user did not start a workflow, they created a new record. The audit log would then indicate that the Java SDK Service Account started the workflow on the user’s behalf.

Document Audit History

The Document Audit History page displays document-related events, including events triggered through the Vault Java SDK. Documents affected by Vault extensions through the use of DocumentService have audit entries identifying the change with Java SDK Service Account on behalf of the user initiating the request. When a user delegates access to another user, the audit will show Java SDK Service Account on behalf of the delegating user.

Sample Code

We’ve created sample code for various use cases. Feel free to use these as starting points for your own custom Vault extensions.

These projects are available at the Veeva GitHub™.

If you need help, please join Vault for Developers on Veeva Connect.

Project Description
Service Basics Services demonstrated in this project include:
  • RecordService
  • QueryService
  • HTTPService
    • Local
    • External
    • Vault to Vault
User-Defined Service Use cases in this project include:
  • User-defined class usage
  • Field validation
  • Updating object record fields
  • UserDefinedService usage
    • Why use a user-defined service
    • User-defined service use cases
    • Locating and using user-defined services
User-Defined Model Use cases in this project include:
  • User-defined model creation and usage
  • Vault to Vault Connections
  • UserDefinedModel usage
    • Why use user-defined models
    • Using user-defined models with HTTPService
    • Using user-defined models with JsonService
    • Using user-defined models with QueryService
Object Records Use cases in this project include:
  • Field defaulting
  • Field validation
  • Required fields
  • Create related records
  • Initialize workflows
  • Update roles
Documents Use cases in this project include:
  • Update fields on related documents
  • Create related object records
  • Send notifications
Job Service Use cases in this project include:
  • Bulk Object Record updates
  • Custom Job Processor Usage
    • Why use custom job processors
    • Use cases in which custom job processors can help
    • Creating custom job processors
Translation Service Use cases in this project include:
  • Message Catalog usage
  • Bulk Translation export and import
  • Field validation
  • TranslationService usage
    • Fetching messages in a message group
    • Fetching translated message
    • Using translated message for error text
Vault to Vault Integration Demonstrates the use of Spark messaging to move data from a source Vault to a target Vault:
  • Basic Spark message
  • Spark message with HTTP Callout
  • Auto-create CrossLink document
Vault to Vault using Integration Rules Demonstrates the use of Spark Messaging to move and mapping data from a source Vault to a target Vault:
  • Spark message with “Poke and Pull” transaction processing
  • Using Integrations, Integration Points, and User Exceptions
  • Mapping field data using Integration Rules and Reference Lookups
Vault to Vault Document Copy Demonstrates copying a document from a source Vault to a target Vault:
  • Spark Message initiated from a Document Lifecycle State Change
  • Creating a new document by reusing an existing document’s source file, attachments, and renditions
  • Creating new versions of existing documents
External Integration Demonstrates the use of Spark messaging to move data from a source Vault to an external AWS™ system:
  • Creating an AWS™ application
  • External Spark message format
  • Receiving and processing an external Spark message

Tools

We’ve created the following tools to assist developers in utilizing the Vault Java SDK.

Tool Description
Maven Plugin Provides commands to package, validate, import, and deploy Vault Java SDK source code through the use of Maven build goals.