Snowfakery recipes that generate Salesforce records are just like any other Snowfakery recipes. You use SObject names for the 'objects'. There are several examples in the Snowfakery repository
The process of actually generating the data into a Salesforce org happens through CumulusCI. The majority of the documentation on using Snowfakery with CumulusCI is in the Generate Data section of the CumulusCI documentation.
A summarized overview follows.
CumulusCI is a tool and framework for building portable automation for Salesforce projects. It is created by the same team that creates Snowfakery.
The easiest way to learn about CumulusCI (and to learn how to install it) is with its Trailhead Trail.
$ cci task run snowfakery examples/salesforce/Contact.recipe.yml --run-until-recipe-repeated 300 --org qa
...
You can (and more often will) use generate_and_load_from_yaml from
within a flow captured in in a cumulusci.yml
.
If you have CumulusCI configured and you would like to test this, you can do so by running some tests that are in the Snowfakery repo itself.
The Snowfakery repo itself has a cumulusci.yml
so it is a
CumulusCI project. You can learn how to set up your own
CumulusCI project by studying it
# clone the repo
$ git clone https://github.com/SFDO-Tooling/Snowfakery.git
# change working directory
$ cd Snowfakery
# run a specific test task
$ cci task run generate_opportunities_and_contacts
# run all of the test tasks and flows
$ cci flow run test_everything
...
Snowfakery and CumulusCI infer the order in which sObjects are loaded
based primarily on which tables have fields that depend on which other
tables. Tables declared earlier in the recipe are also generally loaded
before tables declared later. If you need precise control over load
order, you can control that and many other aspects of the loading process
(e.g. batch size) with a load.yml
file as described in the
CumulusCI docs.
There are various cases where it might be helpful to relate newly created synthetic data to existing data in a Salesforce org. For example, that data might have been added in a previous CumulusCI task or some other process.
For example, if you have a Campaign object and would like to associate Contacts to it through CampaignMembers.
Here is an example where we query a particular Campaign object:
# examples/salesforce/CampaignMembers-first.recipe.yml
- snowfakery_version: 3
- plugin: snowfakery.standard_plugins.Salesforce.SalesforceQuery
- object: Contact
fields:
FirstName: Bobby
LastName: McFerrin
friends:
- object: CampaignMember
fields:
ContactId:
reference: Contact
CampaignId:
SalesforceQuery.find_record:
from: Campaign
where: name='Our Campaign'
Perhaps you do not care which Campaign you connect to:
# examples/salesforce/CampaignMembers-random.recipe.yml
- snowfakery_version: 3
- plugin: snowfakery.standard_plugins.Salesforce.SalesforceQuery
- object: Contact
count: 10
fields:
FirstName:
fake: FirstName
LastName:
fake: LastName
friends:
- object: CampaignMember
fields:
ContactId:
reference: Contact
CampaignId:
SalesforceQuery.random_record:
from: Campaign
As you can see, find_record
looks for a particular record, and returns the first
one that Salesforce finds. The output of find_record
is cached to reduce
unnecessary calls to Salesforce, but the precise rules for how long the
cache lasts are undocumented and may change based on performance testing.
random_record
looks for an random record out of the
first 2000 Salesforce finds. The 2000-record scope limit is based on a Salesforce
limitation and future versions of Snowfakery may incorporate a workaround.
random_record
values are never cached.
NOTE: The features we are discussing in this section are for linking to records that are in the Salesforce org before the recipe iteration started. These features are not for linking to records created by the recipe itself.
Sometimes we want to do more than just link to the other record. For example, perhaps we want to create Users for Contacts and have the Users have the same name as the Contacts.
# examples/salesforce/UsersForContacts.yml
- snowfakery_version: 3
- plugin: snowfakery.standard_plugins.Salesforce.SalesforceQuery
- plugin: snowfakery.standard_plugins.Salesforce
- object: User
fields:
__random_contact:
SalesforceQuery.random_record:
from: Contact
fields: Id, FirstName, LastName
FirstName: ${{__random_contact.FirstName}}
LastName: ${{__random_contact.LastName}}
Alias: Grace
Username:
fake: Username
Email: ${{Username}}
TimeZoneSidKey: America/Bogota
LocaleSidKey: en_US
EmailEncodingKey: UTF-8
LanguageLocaleKey: en_US
ProfileId:
SalesforceQuery.find_record:
from: Profile
where: Name='Identity User'
# ContactId: ${{__random_contact.Id}}
In this case, the actual connection between the contact and
the User is commented out because Identity User
users cannot
have Contacts, but you can see how you would connect a
synthetic object to a pre-existing object, while also getting
access to other fields.
If you would like to use Salesforce query as a Dataset, that's another way that you can ensure that every synthetic record you create is associated with a distinct record from Salesforce.
# examples/soql_dataset.recipe.yml
- snowfakery_version: 3
- plugin: snowfakery.standard_plugins.Salesforce.SOQLDataset
- object: Contact
count: 10
fields:
__users_from_salesforce:
SOQLDataset.iterate:
fields: Id, FirstName, LastName
from: User
OwnerId: ${{__users_from_salesforce.Id}}
FirstName: ${{__users_from_salesforce.FirstName}}
LastName: ${{__users_from_salesforce.LastName}}
Username: TestUser${{fake.Username}}
Or if you'd like them in a random order, you can
use SOQLDataset.shuffle
:
# examples/soql_dataset_shuffled.recipe.yml
- snowfakery_version: 3
- plugin: snowfakery.standard_plugins.Salesforce.SOQLDataset
- object: Contact
count: 10
fields:
__users_from_salesforce:
SOQLDataset.shuffle:
fields: Id, FirstName, LastName
from: User
# The next line depends on the users having particular
# permissions.
OwnerId: ${{__users_from_salesforce.Id}}
FirstName: ${{__users_from_salesforce.FirstName}}
LastName: ${{__users_from_salesforce.LastName}}
Username: TestUser${{fake.Username}}
You may also specify a "where" clause to filter out irrelevant records:
# examples/soql_dataset_where.recipe.yml
- snowfakery_version: 3
- plugin: snowfakery.standard_plugins.Salesforce.SOQLDataset
- object: Contact
count: 10
fields:
__users_from_salesforce:
SOQLDataset.shuffle:
fields: Id, FirstName, LastName
from: User
where: FirstName Like 'A%'
OwnerId: ${{__users_from_salesforce.Id}}
FirstName: ${{__users_from_salesforce.FirstName}}
LastName: ${{__users_from_salesforce.LastName}}
In general, you can test Snowfakery files outside of CumulusCI to see if they work:
$ snowfakery recipe.yml
If you have a recipe which depends on data from an org, specify the CumulusCI org name like this:
$ snowfakery recipe.yml --plugin-options org_name qa
When you run the recipe in this way, it will connect to the org to pull data but not change data in the org at all.
To specify a Record Type for a record, just put the Record Type’s API Name in a field named RecordType.
# tests/cci/record_types.yml
- snowfakery_version: 3
- object: Account
fields:
name: Bluth Family
RecordType: HH_Account
- object: Account
fields:
name: Bluth Corporation
RecordType: Organization
- object: Account
fields:
name: The Windors
RecordType: HH_Account
- object: Account
fields:
name: The Firm
RecordType: Organization
The Salesforce.ProfileId
function looks up a Profile in
Salesforce by name and substitutes the ID.
- plugin: snowfakery.standard_plugins.Salesforce
- object: User
fields:
Alias: Grace
Username:
fake: Username
LastName: Wong
Email: ${{Username}}
TimeZoneSidKey: America/Bogota
LocaleSidKey: en_US
EmailEncodingKey: UTF-8
LanguageLocaleKey: en_US
ProfileId:
Salesforce.ProfileId: Identity User
There are several features planned for the Salesforce Plugin, but the one supported currently is for Person Accounts.
You can use Person Accounts like this:
- plugin: snowfakery.standard_plugins.Salesforce
- object: Account
fields:
FirstName:
fake: FirstName
LastName:
fake: LastName
PersonMailingStreet:
fake: StreetAddress
PersonMailingCity:
fake: City
PersonContactId:
Salesforce.SpecialObject: PersonContact
This will generate a placeholder object in your recipe which can be referred to by other templates like so:
- object: User
fields:
Username:
fake: email
...
ContactId:
reference: Account.PersonContactId
CumulusCI will fix up the references during data load. If you run into errors, please verify that the Account object is being loaded before the others that refer to the PersonContactId. If not, you may need to write a CumulusCI '.load.yml' to ensure that it does.
The Salesforce.SpecialObject
function cannot currently be used for any other
SObject or in any other context. It must always generate a PersonContact
in the PersonContactId
field.
There is also an alternate syntax which allows nicknaming:
- object: Account
fields:
PersonContactId:
Salesforce.SpecialObject:
name: PersonContact
nickname: PCPC
- object: User
fields:
ContactId:
reference: PCPC
Files can be used as Salesforce ContentVersions like this:
# examples/salesforce/ContentVersion.recipe.yml
- snowfakery_version: 3
- plugin: snowfakery.standard_plugins.Salesforce
- object: Account
nickname: FileOwner
fields:
Name:
fake: company
- object: ContentVersion
fields:
Title: Attachment for ${{Account.Name}}
PathOnClient: example.pdf
Description: The example.pdf file
VersionData:
Salesforce.ContentFile:
file: example.pdf
FirstPublishLocationId:
reference: Account
To do updates or upserts in a Salesforce org, refer to the CumulusCI documentation.