Skip to content

generate-scenario

Inspects a compiled Dataverse plugin assembly and emits a ready-to-use scenario JSON file that can be passed directly to xg run. The command launches XrmGhost.FrameworkHost to interrogate the plugin’s metadata — attribute decorations, message handlers, entity configuration — and wires that information into a fully-populated execution context with sensible defaults.

Use generate-scenario as the starting point of your local plugin testing workflow: generate a scaffold once, adjust the generated JSON to match your test data, then pass it to run.


Terminal window
xg generate-scenario -p <PLUGIN_ASSEMBLY_PATH> -t <PLUGIN_TYPE_NAME> [options]

FlagShortTypeDefaultDescription
--plugin-assembly-path-pstring(required)Path to the compiled plugin assembly DLL. The file must exist at the specified path.
--plugin-type-tstring(required)Fully-qualified name of the plugin class to introspect (e.g. MyPlugins.MyPluginClass).
--output-ostringSame directory as the assemblyPath where the generated scenario JSON file is written. If omitted, each file is written alongside the plugin DLL as <PluginTypeName>_<MessageName>_Scenario.json. With one message, a bare filename (no folder) is written to the current directory, and a path with a folder is written exactly there (.json is appended when the path has no extension). With more than one message the filename you pass is ignored: every scenario is named <PluginTypeName>_<MessageName>_Scenario.json and placed in the folder of the path (the current directory for a bare filename), and a warning is printed when -o pointed at a file inside a folder. A directory target — an existing directory, or any value ending with / or \ — always receives the scenarios under their default names, with one message or several and with no warning. See Output location.
--framework-host-path-fstringAuto-resolvedPath to XrmGhost.Framework.Host.exe. When not provided the CLI uses the host bundled in the bin\ folder next to xg.exe first (winget and ZIP installs), then its managed application-data install root, then legacy beside-the-CLI locations.
--debug-mode-dboolfalseEmits detailed verbose output, including FrameworkHost startup information, gRPC health-check status, and per-field scaffold decisions.
--no-default-user-settings—boolfalseSuppresses the automatic insertion of a default usersettings mock (Retrieve response) for the calling user context.
--no-default-owner—boolfalseSuppresses the automatic insertion of a default ownerid (EntityReference to systemuser) in the generated Target entity.
--online—boolfalseRequires an active Pro license: the command verifies the license tier and exits with code 1 if it is not Pro. Scenario generation is otherwise unchanged.
--no-interactive—boolfalseExits immediately if FrameworkHost is not installed instead of downloading it automatically. Useful in CI pipelines.

generate-scenario connects to a short-lived FrameworkHost process over gRPC, sends the plugin assembly path and type name, and receives the plugin’s inferred metadata. From that metadata the command builds an ExecutionContextDefinition that includes:

  • Message name — collected from both the [HandlesMessage] attributes and the Messages of the [PluginExecutionConfig] attributes on the plugin class. The two sources are merged and deduplicated without regard to case (Update and update count once; the first spelling met is kept). If neither family provides a message, the scaffold defaults to Create.
  • Primary entity name, stage and mode — derived from [PluginExecutionConfig] on the plugin, or left empty (stage 20, mode 0) when the plugin carries no metadata. When the class carries several [PluginExecutionConfig] attributes, the first one supplies the primary entity, stage and mode, while the messages of all of them enter the merged message list. No separate scenario is generated per registration: each message gets one scenario built from the first configuration, so check primaryEntityName by hand when your registrations target different entities.
  • Target input parameter — when the plugin declares [InputParameter("Target", …)], the Target comes from that JSON and no name sample is generated; see Target and images from attribute JSON. Otherwise it is auto-generated as an EntityDefinition with a name sample attribute. In both cases it is skipped for Custom API messages (those whose MessageName begins with custom_).
  • ownerid attribute — an EntityReference pointing to the context’s UserId as a systemuser, added to the Target when it does not already carry an ownerid, including a Target taken from [InputParameter]. Suppressed by --no-default-owner.
  • Default UserSettings mock — a Retrieve response that returns a realistic usersettings entity for the calling user. Suppressed by --no-default-user-settings.

[InputParameter("Target", …)], [PreImage] and [PostImage] may carry their entity in the attribute JSON format, where the entity name sits under __entityName and the attributes are flat keys:

{
"__entityName": "account",
"id": "8f2c1a4e-3b6d-4c1e-9a7f-2d5e6b8c9a01",
"name": "Contoso",
"parentaccountid": {
"logicalName": "account",
"id": "1b7e9c3d-5a2f-4e8b-b6c4-7d9a0e1f2c34",
"name": "Contoso Holding"
}
}

The generator does not copy that shape into the scenario: it converts it into the canonical entity of the scenario format, so the file you get back is the one xg run expects.

  • logicalName is the value of __entityName.
  • id is the id key when it holds a GUID; otherwise the <entity>id attribute (for example accountid) when it holds a GUID; otherwise, for the Target, the context’s primaryEntityId. The context’s primaryEntityId is then aligned to the Target id, so both point at the same record.
  • attributes are all the remaining keys, typed the same way as in a scenario file.
  • If __entityName differs from the primary entity of [PluginExecutionConfig], a warning is printed and __entityName wins: it becomes the scenario’s primaryEntityName.
  • The default ownerid is added to this Target too, unless it already has one or --no-default-owner is set.

Pre and post images in the same format follow the same conversion. Their id is the id key or the <entity>id attribute when they hold a GUID; otherwise it is the Target id when the image describes the same entity as the Target, and a new GUID when it describes another entity.

For the example above the scenario contains:

"Target": {
"logicalName": "account",
"id": "8f2c1a4e-3b6d-4c1e-9a7f-2d5e6b8c9a01",
"attributes": {
"name": "Contoso",
"parentaccountid": {
"logicalName": "account",
"id": "1b7e9c3d-5a2f-4e8b-b6c4-7d9a0e1f2c34",
"name": "Contoso Holding"
},
"ownerid": {
"logicalName": "systemuser",
"id": "00000000-0000-0000-0000-0000000000a1",
"name": "Default Owner (System User)"
}
}
}

A lookup attribute is written in the canonical form { "logicalName": "...", "id": "GUID", "name": "..." }, both in the attribute JSON and in the scenario; name is optional:

"primarycontactid": {
"logicalName": "contact",
"id": "4a6b8c0d-2e4f-4a6b-8c0d-2e4f6a8b0c1d",
"name": "Jane Doe"
}

The __entityName shape is only an input the generator converts; scenario files you write by hand use the canonical format.

The file follows the scenario format and contains these top-level keys:

  • scenarioName — Scenario for <PluginTypeName> - <MessageName>.
  • pluginAssemblyName and pluginTypeName.
  • executionContext — the context built above, with Target under inputParameters.
  • organizationServiceMock.retrieveResponses — the default usersettings response, matched on entityName and id and written without a columnSet; the list is empty with --no-default-user-settings.
  • expectedTraceLog — an empty list []: present for discoverability, asserts nothing. Add substrings of the plugin’s traces to turn it into an assertion; the semantics, and the migration of scenarios that still carry the old placeholder line, are in Scenario Files.

A trimmed example (generated ids are random GUIDs; the remaining context fields and most usersettings attributes are omitted):

{
"scenarioName": "Scenario for MyCompany.Plugins.AccountCreatePlugin - Create",
"pluginAssemblyName": "MyCompany.Plugins.dll",
"pluginTypeName": "MyCompany.Plugins.AccountCreatePlugin",
"executionContext": {
"messageName": "Create",
"primaryEntityName": "account",
"primaryEntityId": "00000000-0000-0000-0000-000000000001",
"stage": 20,
"userId": "00000000-0000-0000-0000-0000000000a1",
"inputParameters": {
"Target": {
"logicalName": "account",
"id": "00000000-0000-0000-0000-000000000001",
"attributes": {
"name": "Sample account Name for Create",
"ownerid": {
"logicalName": "systemuser",
"id": "00000000-0000-0000-0000-0000000000a1"
}
}
}
}
},
"organizationServiceMock": {
"retrieveResponses": [
{
"request": {
"entityName": "usersettings",
"id": "00000000-0000-0000-0000-0000000000a1"
},
"response": {
"logicalName": "usersettings",
"id": "00000000-0000-0000-0000-0000000000a1",
"attributes": {
"fullname": "Plugin User",
"uilanguageid": 1033,
"localeid": 1033
}
}
}
]
},
"expectedTraceLog": []
}

The generated file has no root-level entities seed and no expectedException: add them by hand when the plugin reads records through RetrieveMultiple or is expected to throw. The generator does not emit the { "type": "Money", "value": X } tag; with the current release, write that tag only in the entities seed, as explained in Attribute value typing.

--output is classified once, as either a file or a directory target, and that classification decides where every scenario is written. The default name of a scenario is <PluginTypeName>_<MessageName>_Scenario.json.

-o valueClassified asOne messageSeveral messagesWarning
Omitted—Default name in the plugin DLL’s directoryDefault names in the plugin DLL’s directoryNo
Bare file name, e.g. account-create.jsonFileExactly that file, in the current directoryDefault names in the current directoryYes, with several messages
File path, e.g. ./test-scenarios/account-create.jsonFileExactly that fileDefault names in that file’s directoryYes, with several messages
Existing directory, e.g. ./test-scenariosDirectoryDefault name inside the directoryDefault names inside the directoryNo
Value ending with / or \, e.g. ./test-scenarios/DirectoryDefault name inside the directory, created if missingDefault names inside the directory, created if missingNo

A file value without an extension that is not an existing directory stays a file and receives .json: -o account-create writes account-create.json in the current directory.

Directory targets are available from xg 1.7.6 onwards.

The command generates one scenario file per message when the merged list of messages — [HandlesMessage] plus the Messages of every [PluginExecutionConfig], deduplicated without regard to case — contains more than one message. Each file is named <PluginTypeName>_<MessageName>_Scenario.json:

  • without -o, the files go alongside the plugin DLL;
  • with -o and a bare filename, the filename is ignored and the files go to the current directory;
  • with -o and a path inside a folder, the filename is ignored and the files go to that folder, keeping the extension of the path (.json when it has none); a warning reports that -o named a specific file;
  • with -o and a directory target — an existing directory, or a value ending with / or \ — the files go inside that directory under their default names, with no warning; see Output location.

Because the configuration messages count, adding a message to [PluginExecutionConfig] can turn a command that wrote -o ./test-scenarios/account-create.json into a fan-out that writes ./test-scenarios/<PluginTypeName>_<MessageName>_Scenario.json files instead. With a single message, -o keeps writing exactly the file you named.

If required metadata is missing (e.g. no [HandlesMessage] attribute and no PrimaryEntityName set), the command emits a warning after writing the file:

Warning (Message: Create): Generated a partial scenario scaffold because required
XrmGhost metadata was missing or incomplete (executionContext.primaryEntityName,
inputParameters.Target.logicalName). Before using this scenario, fill those values manually.

This warning does not prevent the file from being written. It flags that you need to review and complete the scaffold before running it.

--online requires an active Pro license. The command verifies the license tier before proceeding and exits with code 1 if the tier is not Pro. Otherwise, the scenario is generated exactly as it is without --online.


CodeMeaning
0All scenario files generated successfully.
1An error occurred: assembly not found, type load failure, license check failed (online mode), or FrameworkHost startup failure.

Minimal — generate a scenario for a single plugin type

Section titled “Minimal — generate a scenario for a single plugin type”
Terminal window
xg generate-scenario \
--plugin-assembly-path ./bin/Debug/net8.0/MyCompany.Plugins.dll \
--plugin-type MyCompany.Plugins.AccountCreatePlugin

The scenario file is written to ./bin/Debug/net8.0/MyCompany.Plugins.AccountCreatePlugin_Create_Scenario.json.

Terminal window
xg generate-scenario \
-p ./bin/Debug/net8.0/MyCompany.Plugins.dll \
-t MyCompany.Plugins.AccountCreatePlugin \
-o ./test-scenarios/account-create.json

The plugin handles one message, so the scenario is written exactly to ./test-scenarios/account-create.json: the file name you pass is kept. To get the default name inside a folder instead, pass the folder, e.g. -o ./test-scenarios/.

Generate without the default owner and user-settings mock

Section titled “Generate without the default owner and user-settings mock”
Terminal window
xg generate-scenario \
-p ./bin/Debug/net8.0/MyCompany.Plugins.dll \
-t MyCompany.Plugins.AccountCreatePlugin \
--no-default-owner \
--no-default-user-settings
Terminal window
xg generate-scenario \
-p ./artifacts/MyCompany.Plugins.dll \
-t MyCompany.Plugins.AccountCreatePlugin \
--no-interactive

Enable debug output to diagnose FrameworkHost startup

Section titled “Enable debug output to diagnose FrameworkHost startup”
Terminal window
xg generate-scenario \
-p ./bin/Debug/net8.0/MyCompany.Plugins.dll \
-t MyCompany.Plugins.AccountCreatePlugin \
--debug-mode