-
-
Notifications
You must be signed in to change notification settings - Fork 0
14 Migration from spkl
Flowline is the natural successor to spkl. spkl is effectively abandoned (last commit 2021).
Flowline covers the same attribute-driven plugin registration and web resource push, and extends it with Custom APIs, a single-assembly model, and a full Git-based ALM workflow.
This guide covers migrating an existing spkl project to Flowline.
| spkl | Flowline |
|---|---|
[CrmPluginRegistration(...)] |
[Step], [Filter], [PreImage], [PostImage]
|
spkl plugins |
flowline push --scope plugins |
spkl webresources |
flowline push --scope webresources |
spkl plugins + spkl webresources
|
flowline push |
spkl earlybound |
flowline generate |
spkl unpack |
flowline clone (first time) / flowline sync
|
spkl import |
flowline deploy |
spkl whoami |
flowline status |
spkl instrument |
No equivalent. flowline clone only covers the web resource side |
spkl.json |
.flowline (environment URLs only) |
| Windows Credential Manager / connection strings | pac auth create |
- Install PAC CLI:
dotnet tool install --global Microsoft.PowerApps.CLI.Tool - Install Flowline:
dotnet tool install --global Flowline - Authenticate:
pac auth create --environment https://your-org.crm4.dynamics.com - Verify:
flowline status
Remove the spkl NuGet package from your project after you have replaced all [CrmPluginRegistration]
attributes, and both packages can coexist during migration.
Running everything in one pass is possible, but splitting the migration into two phases reduces risk. Only one thing changes at a time, so problems are easier to diagnose.
Phase 1, standalone (no project restructuring required)
Replace spkl for plugin registration, web resource sync, and type generation using Flowline's standalone mode. Flowline reads your existing build output directly, so you need no .flowline config and no folder restructuring. Run these from a folder that does not contain a .flowline file.
# Plugins only
flowline push MySolution --pluginFile ./bin/Release/MyPlugins.dll --dev https://your-org.crm4.dynamics.com
# Web resources only
flowline push MySolution --webresources ./WebResources/dist --dev https://your-org.crm4.dynamics.com
# Both at once
flowline push MySolution --pluginFile ./bin/Release/MyPlugins.dll --webresources ./WebResources/dist --dev https://your-org.crm4.dynamics.com
# Type generation
flowline generate MySolution --namespace MyNamespace --output ./Models --dev https://your-org.crm4.dynamics.comAlways verify with --dry-run first to confirm Flowline's plan matches what spkl was registering. Once phase 1 is stable, spkl is no longer needed for day-to-day pushes.
Phase 2, project structure (when you're ready)
Run flowline clone <solution> --prod <url> to create the .flowline config and adopt the Flowline folder convention (Plugins/, WebResources/ at the project root, with no wrapper folder). The environment flag is required on this first run, since there's no .flowline yet to read one from. From this point, flowline push reads from the project rather than requiring explicit flags.
clone is the right command here: your solution already exists in Dataverse and clone adopts it. flowline init is for greenfield only. It creates a new publisher and solution, and would refuse a name that already exists.
The steps below cover Phase 2 in detail.
This is the main migration task. Flowline splits the single [CrmPluginRegistration] constructor into
focused attributes, and encodes stage and message in the class name.
Replace the spkl attributes package with Flowline.Attributes:
<!-- Remove -->
<PackageReference Include="spkl" Version="..." />
<!-- Add -->
<PackageReference Include="Flowline.Attributes" Version="1.0.0" PrivateAssets="all" />Remove CrmPluginConfigurationAttribute.cs if spkl added it to your project. The NuGet package replaces it.
spkl puts everything in one attribute on any class name. Flowline reads the stage and message from the class name. You pick the naming; Flowline parses it.
| Stage | Message | Class name pattern |
|---|---|---|
| PreValidation | any | {Name}Validation{Message}[Plugin] |
| PreOperation | any | {Name}Pre{Message}[Plugin] |
| PostOperation sync | any | {Name}Post{Message}[Plugin] |
| PostOperation async | any | {Name}Post{Message}Async[Plugin] |
Examples:
| spkl attribute args | Flowline class name |
|---|---|
"Update", PreOperation, Synchronous |
AccountPreUpdatePlugin |
"Create", PostOperation, Synchronous |
AccountPostCreatePlugin |
"Delete", PreValidation, Synchronous |
ContactValidationDeletePlugin |
"Update", PostOperation, Asynchronous |
InvoicePostUpdateAsyncPlugin |
Can't rename the class? Use [Handles] to declare message and stage explicitly, with no rename:
// spkl: any class name, stage in attribute
[CrmPluginRegistration("Update", "account", StageEnum.PreOperation, ExecutionModeEnum.Synchronous, ...)]
public class AccountPlugin : IPlugin { ... }
// Flowline: class name stays, [Handles] declares stage and message
[Step("account")]
[Handles(Message.Update, Stage.PreOperation)]
public class AccountPlugin : IPlugin { ... }// spkl
[CrmPluginRegistration("Update", "account", StageEnum.PreOperation, ExecutionModeEnum.Synchronous,
"name,creditlimit", "Account Pre Update", 1, IsolationModeEnum.Sandbox)]
public class AccountPlugin : IPlugin { ... }
// Flowline
[Step("account", Order = 1)]
[Filter("name", "creditlimit")]
public class AccountPreUpdatePlugin : IPlugin { ... }// spkl
[CrmPluginRegistration("Update", "account", StageEnum.PostOperation, ExecutionModeEnum.Synchronous,
"name", "Account Post Update", 1, IsolationModeEnum.Sandbox,
Image1Name = "preimage", Image1Type = ImageTypeEnum.PreImage, Image1Attributes = "name,creditlimit",
Image2Name = "postimage", Image2Type = ImageTypeEnum.PostImage, Image2Attributes = "name,creditlimit")]
public class AccountPlugin : IPlugin { ... }
// Flowline
[Step("account")]
[Filter("name")]
[PreImage("name", "creditlimit")]
[PostImage("name", "creditlimit")]
public class AccountPostUpdatePlugin : IPlugin { ... }// spkl
[CrmPluginRegistration("Update", "cr07982_invoice", StageEnum.PostOperation, ExecutionModeEnum.Asynchronous,
"cr07982_status", "Invoice Post Update Async", 1, IsolationModeEnum.Sandbox,
DeleteAsyncOperation = true)]
public class InvoicePlugin : IPlugin { ... }
// Flowline — DeleteJobOnSuccess defaults to true, no need to set it explicitly
[Step("cr07982_invoice")]
[Filter("cr07982_status")]
public class InvoicePostUpdateAsyncPlugin : IPlugin { ... }
[CrmPluginRegistration] property |
Flowline equivalent |
|---|---|
Message (1st arg) |
Class name (Pre, Post, Validation + message keyword) |
EntityLogicalName (2nd arg) |
[Step("account")] |
Stage (3rd arg) |
Class name keyword |
ExecutionMode (4th arg) |
Async suffix in class name |
FilteringAttributes (5th arg) |
[Filter("col1", "col2")] |
stepName (6th arg) |
Not needed. Flowline generates step names |
ExecutionOrder (7th arg) |
[Step(..., Order = 1)] |
IsolationMode (8th arg) |
Flowline only supports online only (Sandbox) |
UnSecureConfiguration |
[Step(..., Config = "...")] |
SecureConfiguration |
No equivalent, you don't want to store sensitive information in code |
Image1Name/Type/Attributes |
[PreImage("col1", "col2")] |
Image2Name/Type/Attributes |
[PostImage("col1", "col2")] |
DeleteAsyncOperation |
[Step(..., DeleteJobOnSuccess = true)] (default: true) |
Description |
Not mapped. Flowline stamps [flowline] in description |
Id (step GUID) |
No equivalent. Flowline matches by generated step name |
spkl supports stacking multiple [CrmPluginRegistration] attributes on one class:
// spkl — one class, two step registrations
[CrmPluginRegistration("Create", "account", StageEnum.PostOperation, ExecutionModeEnum.Synchronous,
null, "Account Post Create", 1, IsolationModeEnum.Sandbox)]
[CrmPluginRegistration("Update", "account", StageEnum.PostOperation, ExecutionModeEnum.Synchronous,
"name,creditlimit", "Account Post Update", 1, IsolationModeEnum.Sandbox)]
public class AccountPlugin : IPlugin { ... }Flowline's convention requires one class per step. During migration, stack [Handles] allows you to register multiple steps if the table in the [Step] attribute is the same:
// Flowline — temporary multi-step form using stacked [Handles]
[Step("account")]
[Filter("name", "creditlimit")]
[Handles(Message.Create, Stage.PostOperation)]
[Handles(Message.Update, Stage.PostOperation)]
public class AccountPlugin : IPlugin { ... }Flowline emits a warning (once per class) on every push nudging you to split into named subclasses. The long-term form:
[Step("account")]
public class AccountPostCreatePlugin : IPlugin { ... }
[Step("account")]
[Filter("name", "creditlimit")]
public class AccountPostUpdatePlugin : IPlugin { ... }Splitting warning: splitting the class renames the Dataverse steps. The next flowline push deletes the old steps and creates new ones. Plan the split for a maintenance window to avoid step downtime.
spkl requires an explicit file mapping in spkl.json:
{
"webresources": [{
"root": "",
"solution": "MySolution",
"files": [
{ "uniquename": "new_/js/utils.js", "file": "js\\utils.js" },
{ "uniquename": "new_mysolution/js/form.js", "file": "js\\form.js" }
]
}]
}Flowline derives the Dataverse name from the folder structure, with no mapping file:
// default (files in the root of dist are prefixed with the plubisher and solution name)
WebResources/dist/js/form.js → new_mysolution/js/form.js
// or explicit subfolder (Flowline uses the subfolder name as-is)
WebResources/dist/new_/js/utils.js → new_/js/utils.js
WebResources/dist/new_mysolution/js/form.js → new_mysolution/js/form.js
Convention: {publisher_prefix}_{solution_name}/{relative_path_under_dist}
- Copy your web resource files into
WebResources/dist/maintaining the same subfolder structure. - Run
flowline push --scope webresources --dry-runto preview what Flowline will create.
# spkl
spkl earlybound
# Flowline
flowline generatespkl used CrmSvcUtil (the old SDK tool). Flowline wraps pac modelbuilder build with opinionated defaults.
spkl earlyboundtypes section in spkl.json:
{
"earlyboundtypes": [{
"entities": "account,contact,quote",
"actions": "dev1_simpleaction",
"generateOptionsetEnums": true,
"filename": "EarlyBoundTypes.cs",
"classNamespace": "TestPlugin"
}]
}Flowline equivalent, run once and saved to .flowline:
flowline generate --namespace TestPlugin --extra-tables account,contact,quoteThe namespace and extra tables are saved to .flowline after the first run, so later flowline generate
calls pick them up automatically. No separate config file.
Generated types land in Plugins/Models/ alongside your plugin code.
spkl stored its own credentials or accepted connection strings on the command line.
Flowline reuses the PAC CLI token cache, so you get modern OAuth without passwords:
# Authenticate once
pac auth create --environment https://your-org.crm4.dynamics.com
# In CI/CD — service principal
pac auth create --kind ServicePrincipal --applicationId $CLIENT_ID --clientSecret $CLIENT_SECRET --tenant $TENANT_IDRun flowline status to verify the active auth profile.
Once all attributes are replaced and you have verified flowline push works, delete spkl.json.
The .flowline file holds environment URLs; plugin and web resource paths are derived from project structure.