Skip to content

13 Migration from spkl

RemyDuijkeren edited this page Jul 17, 2026 · 2 revisions

Migration from spkl

Flowline is the natural successor to spkl. spkl is effectively abandoned (last commit 2021), targets .NET Framework 4.6.2, and will never get Custom API support or .NET 10. 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 — see below
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 (Windows alternative: winget install Microsoft.PowerAppsCLI)
  • 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 — 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. No .flowline config, no folder restructuring — Flowline reads your existing build output directly. 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 init to create the .flowline config and adopt the Flowline folder convention (Plugins/, WebResources/ at the project root — no wrapper folder). From this point, flowline push reads from the project rather than requiring explicit flags.

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.

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 — no rename required:

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

With unsecure config

// spkl
[CrmPluginRegistration(..., UnSecureConfiguration = "{\"endpoint\":\"https://api.example.com\"}")]

// Flowline
[Step("account", Config = "{\"endpoint\":\"https://api.example.com\"}")]

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

Custom action (not Custom API)

// spkl — registers as a plugin step on a custom message
[CrmPluginRegistration(
    "dev1_TestAction", "account",
    StageEnum.PreOperation, ExecutionModeEnum.Synchronous,
    null, "dev1_TestAction", 1000, IsolationModeEnum.Sandbox)]
public class TestActionPlugin : IPlugin { ... }

// Flowline — use [Handles] with string message for custom action messages
[Step("account")]
[Handles("dev1_TestAction", Stage.PreOperation)]
public class TestActionPlugin : IPlugin { ... }

Custom API

// spkl — only links an existing Custom API record to the plugin type; does not create the API
[CrmPluginRegistration("dev1_MyCustomApi")]
public class MyCustomApiPlugin : IPlugin { ... }

// Flowline — creates and manages the Custom API record in Dataverse
[CustomApi]
public class MyCustomApiPlugin : IPlugin { ... }

The class stays unchanged during migration — copy [Input]/[Output] declarations across and swap the linking attribute for [CustomApi]. If the live Custom API's unique name doesn't happen to match what Flowline's class-name convention would derive (spkl's [CrmPluginRegistration] links to an arbitrary pre-existing name, unrelated to the class name), use UniqueName to pin the exact same value so push updates the existing record instead of creating a duplicate:

// spkl — linked name doesn't match what Flowline would derive from the class name
[CrmPluginRegistration("dev1_ApproveOrder")]
public class LegacyOrderApprovalPlugin : IPlugin { ... }

// Flowline — pin the same live unique name explicitly
[CustomApi(UniqueName = "dev1_ApproveOrder")]
public class LegacyOrderApprovalPlugin : IPlugin { ... }

See 04-Push-Plugins-and-Custom-APIs#custom-apis for full [CustomApi], [Input], and [Output] reference.

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) Not needed — hardcoded Sandbox (online only)
UnSecureConfiguration [Step(..., Config = "...")]
SecureConfiguration No equivalent — see Known gaps
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) Not needed — 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] to register multiple steps without restructuring:

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

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 — it is replaced by the NuGet package.


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_/js/form.js",  "file": "js\\form.js" }
    ]
  }]
}

Flowline derives the Dataverse name from the folder structure — no mapping file:

WebResources/dist/js/utils.js  →  new_mysolution/js/utils.js
WebResources/dist/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.

Renaming existing Dataverse records

If your existing web resources use names that don't match Flowline's convention (e.g. new_/js/utils.js instead of new_mysolution/js/utils.js), you have two options:

Option A — Rename in Dataverse (recommended for new projects):
Update the web resource's unique name in the maker portal or via the API to match the convention, then update any form scripts or ribbon actions that reference the old name. Run flowline push — Flowline will find and update the records by their new names.

Option B — Keep old names with --no-delete during transition:
Push Flowline's convention-named records first (flowline push --scope webresources), then manually delete the old records from Dataverse once all references are updated. Use --no-delete while both old and new records coexist to avoid Flowline deleting the old ones prematurely.

Tip: Use --dry-run throughout the transition to verify what Flowline plans to do before it touches Dataverse.

See 08-WebResources-Project for a full guide to the TypeScript setup, Rollup build pipeline, and folder structure.


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, 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 — subsequent flowline generate calls pick them up automatically. No separate config file.

Generated types land in Plugins/Models/ alongside your plugin code.


Step 4 — Replace commands

Push plugins

# spkl
spkl plugins

# Flowline
flowline push --scope plugins

# or push everything (plugins + web resources) in one command
flowline push

Push web resources

# spkl
spkl webresources

# Flowline
flowline push --scope webresources

Assembly-only push (fast inner loop)

# spkl — no equivalent
# Flowline — updates only the DLL bytes, does not touch step or Custom API registrations
flowline push --scope assemblyonly

Unpack solution

# spkl
spkl unpack

# Flowline — first time: bootstraps the repo
flowline clone ContosoSales --prod https://contoso.crm4.dynamics.com

# Flowline — ongoing: pull dev changes into source control
flowline sync

Pack and import solution

# spkl
spkl import

# Flowline
flowline deploy test
flowline deploy prod

Verify connection

# spkl
spkl whoami

# Flowline
flowline status

Step 5 — Replace authentication

spkl stored credentials in Windows Credential Manager (%APPDATA%\CrmServer\Credentials.xml) or accepted connection strings on the command line.

Flowline reuses the PAC CLI token cache — modern OAuth, no 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 6 — 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.


No instrument equivalent

spkl's instrument command downloaded existing plugin step registrations from Dataverse and reverse-engineered them into [CrmPluginRegistration] attributes on your source files. Flowline has no equivalent for the plugin side — but flowline clone covers the web resource side: it downloads all web resources in the solution and places them in the WebResources/ project automatically. Plugin attribute instrumentation is not yet supported.

Manual process for existing plugin registrations:

  1. Open Plugin Registration Tool and review the existing steps for your assembly.
  2. For each registered step, add [Step], [Filter], [PreImage], and [PostImage] attributes to the corresponding class using the mapping table above.
  3. Run flowline push --dry-run to verify Flowline's parsed registration matches what is in Dataverse before doing a live push.

Tip: rename classes to follow the convention (AccountPreUpdatePlugin) as you go — this makes [Handles] unnecessary and the registration self-documenting.


Known gaps

SecureConfiguration

spkl supports SecureConfiguration on plugin steps — a separately encrypted field in Dataverse for storing API keys or other sensitive values. Flowline only has Config (the unsecureConfig field).

Workaround: Use Azure Key Vault or environment variables for sensitive config. If you must keep secure config on existing steps, update those steps manually in Plugin Registration Tool — Flowline will not overwrite fields it does not manage.

Multi-assembly projects

If your spkl project deploys multiple assemblies (plugins in one DLL, workflow activities in another), consolidate them into a single Plugins project. Flowline reads all IPlugin, CodeActivity, and [CustomApi] types from one assembly in a single pass — no split project needed.

See 04-Push-Plugins-and-Custom-APIs#one-assembly-for-everything for the rationale.

Profiles

spkl's profile system (/p:debug, /p:release) mapped to different assemblypath entries in spkl.json. Flowline uses the standard .NET build configuration instead — build in Release before pushing:

dotnet build -c Release
flowline push

Clone this wiki locally