Skip to content

14 Migration from spkl

Remy van Duijkeren edited this page Aug 22, 2026 · 1 revision

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.


What maps to what

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

Before you start

  • 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.


Two-phase migration strategy

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.com

Always 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.


Step 1: replace plugin registration attributes

This is the main migration task. Flowline splits the single [CrmPluginRegistration] constructor into focused attributes, and encodes stage and message in the class name.

Install Flowline.Attributes

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.

Class naming

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 { ... }

Attribute mapping

Basic step

// 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 { ... }

With images

// 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 { ... }

Async step

// 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 { ... }

Full attribute mapping reference

[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

One class, multiple step registrations

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.


Step 2: replace web resource configuration

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}

Moving files

  1. Copy your web resource files into WebResources/dist/ maintaining the same subfolder structure.
  2. Run flowline push --scope webresources --dry-run to preview what Flowline will create.

Step 3: replace early-bound type generation

# spkl
spkl earlybound

# Flowline
flowline generate

spkl used CrmSvcUtil (the old SDK tool). Flowline wraps pac modelbuilder build with opinionated defaults.

Config migration

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,quote

The 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.


Step 4: replace authentication

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_ID

Run flowline status to verify the active auth profile.


Step 5: remove spkl.json

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.

Clone this wiki locally