generate-scenario
xg generate-scenario
Section titled “xg 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.
Synopsis
Section titled “Synopsis”xg generate-scenario -p <PLUGIN_ASSEMBLY_PATH> -t <PLUGIN_TYPE_NAME> [options]Options
Section titled “Options”| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--plugin-assembly-path | -p | string | (required) | Path to the compiled plugin assembly DLL. The file must exist at the specified path. |
--plugin-type | -t | string | (required) | Fully-qualified name of the plugin class to introspect (e.g. MyPlugins.MyPluginClass). |
--output | -o | string | Same directory as the assembly | Path 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 | -f | string | Auto-resolved | Path 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 | -d | bool | false | Emits detailed verbose output, including FrameworkHost startup information, gRPC health-check status, and per-field scaffold decisions. |
--no-default-user-settings | — | bool | false | Suppresses the automatic insertion of a default usersettings mock (Retrieve response) for the calling user context. |
--no-default-owner | — | bool | false | Suppresses the automatic insertion of a default ownerid (EntityReference to systemuser) in the generated Target entity. |
--online | — | bool | false | Requires 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 | — | bool | false | Exits immediately if FrameworkHost is not installed instead of downloading it automatically. Useful in CI pipelines. |
Description
Section titled “Description”How the scaffold is built
Section titled “How the scaffold is built”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 theMessagesof the[PluginExecutionConfig]attributes on the plugin class. The two sources are merged and deduplicated without regard to case (Updateandupdatecount once; the first spelling met is kept). If neither family provides a message, the scaffold defaults toCreate. - Primary entity name, stage and mode — derived from
[PluginExecutionConfig]on the plugin, or left empty (stage20, mode0) 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 checkprimaryEntityNameby hand when your registrations target different entities. Targetinput parameter — when the plugin declares[InputParameter("Target", …)], the Target comes from that JSON and nonamesample is generated; see Target and images from attribute JSON. Otherwise it is auto-generated as anEntityDefinitionwith anamesample attribute. In both cases it is skipped for Custom API messages (those whoseMessageNamebegins withcustom_).owneridattribute — anEntityReferencepointing to the context’sUserIdas asystemuser, added to theTargetwhen it does not already carry anownerid, including a Target taken from[InputParameter]. Suppressed by--no-default-owner.- Default UserSettings mock — a Retrieve response that returns a realistic
usersettingsentity for the calling user. Suppressed by--no-default-user-settings.
Target and images from attribute JSON
Section titled “Target and images from attribute JSON”[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.
logicalNameis the value of__entityName.idis theidkey when it holds a GUID; otherwise the<entity>idattribute (for exampleaccountid) when it holds a GUID; otherwise, for the Target, the context’sprimaryEntityId. The context’sprimaryEntityIdis then aligned to the Targetid, so both point at the same record.attributesare all the remaining keys, typed the same way as in a scenario file.- If
__entityNamediffers from the primary entity of[PluginExecutionConfig], a warning is printed and__entityNamewins: it becomes the scenario’sprimaryEntityName. - The default
owneridis added to this Target too, unless it already has one or--no-default-owneris 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.
Shape of the generated file
Section titled “Shape of the generated file”The file follows the scenario format and contains these top-level keys:
scenarioName—Scenario for <PluginTypeName> - <MessageName>.pluginAssemblyNameandpluginTypeName.executionContext— the context built above, withTargetunderinputParameters.organizationServiceMock.retrieveResponses— the defaultusersettingsresponse, matched onentityNameandidand written without acolumnSet; 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 location
Section titled “Output location”--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 value | Classified as | One message | Several messages | Warning |
|---|---|---|---|---|
| Omitted | — | Default name in the plugin DLL’s directory | Default names in the plugin DLL’s directory | No |
Bare file name, e.g. account-create.json | File | Exactly that file, in the current directory | Default names in the current directory | Yes, with several messages |
File path, e.g. ./test-scenarios/account-create.json | File | Exactly that file | Default names in that file’s directory | Yes, with several messages |
Existing directory, e.g. ./test-scenarios | Directory | Default name inside the directory | Default names inside the directory | No |
Value ending with / or \, e.g. ./test-scenarios/ | Directory | Default name inside the directory, created if missing | Default names inside the directory, created if missing | No |
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.
Multiple message handlers
Section titled “Multiple message handlers”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
-oand a bare filename, the filename is ignored and the files go to the current directory; - with
-oand a path inside a folder, the filename is ignored and the files go to that folder, keeping the extension of the path (.jsonwhen it has none); a warning reports that-onamed a specific file; - with
-oand 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.
Partial scaffold warning
Section titled “Partial scaffold warning”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 requiredXrmGhost 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 mode
Section titled “Online mode”--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.
Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
0 | All scenario files generated successfully. |
1 | An error occurred: assembly not found, type load failure, license check failed (online mode), or FrameworkHost startup failure. |
Examples
Section titled “Examples”Minimal — generate a scenario for a single plugin type
Section titled “Minimal — generate a scenario for a single plugin type”xg generate-scenario \ --plugin-assembly-path ./bin/Debug/net8.0/MyCompany.Plugins.dll \ --plugin-type MyCompany.Plugins.AccountCreatePluginThe scenario file is written to ./bin/Debug/net8.0/MyCompany.Plugins.AccountCreatePlugin_Create_Scenario.json.
Specify an explicit output path
Section titled “Specify an explicit output path”xg generate-scenario \ -p ./bin/Debug/net8.0/MyCompany.Plugins.dll \ -t MyCompany.Plugins.AccountCreatePlugin \ -o ./test-scenarios/account-create.jsonThe 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”xg generate-scenario \ -p ./bin/Debug/net8.0/MyCompany.Plugins.dll \ -t MyCompany.Plugins.AccountCreatePlugin \ --no-default-owner \ --no-default-user-settingsNon-interactive mode (CI pipeline)
Section titled “Non-interactive mode (CI pipeline)”xg generate-scenario \ -p ./artifacts/MyCompany.Plugins.dll \ -t MyCompany.Plugins.AccountCreatePlugin \ --no-interactiveEnable debug output to diagnose FrameworkHost startup
Section titled “Enable debug output to diagnose FrameworkHost startup”xg generate-scenario \ -p ./bin/Debug/net8.0/MyCompany.Plugins.dll \ -t MyCompany.Plugins.AccountCreatePlugin \ --debug-modeSee Also
Section titled “See Also”xg run— Execute a scenario file produced bygenerate-scenario- CLI Overview — Introduction to the
xgCLI and command groups - Getting Started with the CLI — Installation, verification, and your first command
- CLI Reference — Full reference index for all CLI commands