-
-
Notifications
You must be signed in to change notification settings - Fork 0
14 Migration from Daxif
Daxif (Delegate Automated Xrm Installation Framework) is an F# library from Delegate A/S that provides a programmable Dataverse deployment pipeline via F# scripts. It is in maintenance mode (last meaningful release 2024) and requires F# tooling and knowledge to operate.
Flowline covers the same core operations — plugin registration, web resource sync, solution pack/import, early-bound type generation — as a conventional CLI tool with no F# knowledge required.
| Daxif | Flowline |
|---|---|
Plugin.Sync(env, dll, "", solution) |
flowline push --scope plugins |
WebResource.Sync(env, folder, solution) |
flowline push --scope webresources |
| Plugin + web resource sync | flowline push |
Solution.Export(env, solution, outDir) |
flowline sync |
Solution.Import(env, zipPath) |
flowline deploy test / flowline deploy prod
|
Solution.UpdateVersionNumber(...) |
Built into flowline sync --bump
|
Solution.Extract(...) |
Built into flowline clone / flowline sync
|
Solution.Pack(...) |
Built into flowline deploy
|
Solution.GenerateCSharpContext(...) |
flowline generate --generator xrmcontext3 |
Info.RetrieveVersion(env) |
flowline status |
_Config.fsx environment definitions |
.flowline config file |
| Windows Credential Manager / OAuth config | pac auth create |
ExtendedSolution (pre/post import) |
Pre-import orphan cleanup built into flowline deploy (automatic) — see Extended Solution
|
- 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
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 Daxif 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.comAlways verify with --dry-run first to confirm Flowline's plan matches what Daxif was registering. Once phase 1 is stable, Daxif 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.
This is the most significant migration task. Daxif requires plugins to inherit from its Plugin base
class and declare steps in the constructor via a fluent API. Flowline uses attributes and detects any
class that implements IPlugin — directly or through a base class — anywhere in its inheritance chain.
Your own PluginBase : IPlugin works fine; Flowline will pick it up.
// Daxif — Plugin base class + fluent API in constructor
public class MyAccountPlugin : Plugin
{
public MyAccountPlugin()
{
RegisterPluginStep<Account>(EventOperation.Update, ExecutionStage.PostOperation, Execute)
.SetExecutionMode(ExecutionMode.Asynchronous)
.AddFilteredAttributes(x => x.Address1_City, x => x.Address1_Country)
.AddImage(ImageType.PreImage, x => x.Address1_Country);
}
private void Execute(LocalPluginContext ctx) { ... }
}
// Flowline — attributes on any class that implements IPlugin (directly or via a base class)
[Step("account")]
[Filter("address1_city", "address1_country")]
[PreImage("address1_country")]
public class AccountPostUpdateAsyncPlugin : IPlugin
{
public void Execute(IServiceProvider sp) { ... }
}| Daxif fluent API | Flowline equivalent |
|---|---|
RegisterPluginStep<Account>(...) |
[Step("account")] |
EventOperation.Update |
Class name: ...Update...
|
ExecutionStage.PreValidation |
Class name: ...Validation...
|
ExecutionStage.PreOperation |
Class name: ...Pre...
|
ExecutionStage.PostOperation |
Class name: ...Post...
|
SetExecutionMode(Asynchronous) |
Class name: ...Async... suffix |
AddFilteredAttributes(x => x.Field) |
[Filter("field")] |
AddImage(ImageType.PreImage, x => x.Field) |
[PreImage("field")] |
AddImage(ImageType.PostImage, x => x.Field) |
[PostImage("field")] |
SetExecutionOrder(1) |
[Step(..., Order = 1)] |
SetImpersonatingUser(guid) |
[Step(..., RunAs = "guid")] |
SetDeployment(ServerOnly) |
Not needed — Flowline targets online only |
SetDescription(...) |
Not mapped |
Stage and message come from the class name. See 04-Push-Plugins-and-Custom-APIs for the full naming reference.
| Daxif stage + message | Flowline class name |
|---|---|
PostOperation + Update + async |
AccountPostUpdateAsyncPlugin |
PreOperation + Update
|
AccountPreUpdatePlugin |
PostOperation + Create
|
AccountPostCreatePlugin |
PreValidation + Delete
|
ContactValidationDeletePlugin |
Can't rename the class? Use [Handles] to declare stage and message explicitly — the class name is irrelevant:
[Step("account")]
[Handles(Message.Update, Stage.PreOperation)]
public class AccountPlugin : IPlugin { ... }Daxif's fluent API lets one constructor register multiple steps from one class:
// Daxif — two registrations from one class
public class AccountPlugin : Plugin
{
public AccountPlugin()
{
RegisterPluginStep<Account>(EventOperation.Create, ExecutionStage.Post, Execute);
RegisterPluginStep<Account>(EventOperation.Update, ExecutionStage.Post, Execute)
.AddFilteredAttributes(x => x.Name, x => x.CreditLimit);
}
private void Execute(LocalPluginContext ctx) { ... }
}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.
Daxif uses a CustomAPI base class. Flowline uses attributes:
// Daxif
public class GetAccountRiskApi : CustomAPI
{
public GetAccountRiskApi()
: base(typeof(GetAccountRiskApi))
{
RegisterCustomAPI("GetAccountRiskApi", CalculateRisk)
.EnableCustomization()
.AddRequestParameter(new CustomAPIConfig.CustomAPIRequestParameter(
"accountId", RequestParameterType.EntityReference, "account"))
.AddResponseProperty(new CustomAPIConfig.CustomAPIResponseProperty(
"riskScore", RequestParameterType.Integer));
}
protected void CalculateRisk(LocalPluginContext localContext)
{
var pluginContext = localContext.PluginExecutionContext;
var accountRef = (EntityReference)pluginContext.InputParameters["accountId"];
// ... calculate risk ...
pluginContext.OutputParameters["riskScore"] = 42;
}
}
// Flowline
[CustomApi]
[Input("accountId", FieldType.EntityReference, Table = "account")]
[Output("riskScore", FieldType.Integer)]
public class GetAccountRiskApi : IPlugin
{
public void Execute(IServiceProvider sp) { ... }
}If a Custom API is already live under a name that doesn't match what Flowline's class-name convention would derive, use UniqueName to pin the exact same value — Flowline validates it against the solution's publisher prefix and updates the existing record in place instead of creating a duplicate:
// Live Custom API is "dev1_ApproveOrder", but the class doesn't derive to that name
[CustomApi(UniqueName = "dev1_ApproveOrder")]
public class LegacyOrderApprovalPlugin : IPlugin { ... }See 04-Push-Plugins-and-Custom-APIs#custom-apis for the full attribute reference.
Add to your Plugins project:
<PackageReference Include="Flowline.Attributes" Version="1.0.0" PrivateAssets="all" />Remove the Daxif NuGet package from your project once all registrations are converted.
Daxif silently ignores any IPlugin class that does not inherit from its Plugin base class. If your
project has plugins that bypass the base class, they are not registered by Daxif today — but Flowline
will pick them up (any IPlugin implementor is eligible). Review for unintended registrations after
the first flowline push --dry-run.
The naming convention is the same in both tools:
{publisher_prefix}_{solution_name}/{relative_path}
Example: dg_MySolution/js/utils.js
The difference is where files live on disk. Daxif scans a folder that already contains the full
prefixed path. Flowline scans WebResources/dist/ and prepends the prefix and solution name:
Daxif: webResources/dg_MySolution/js/utils.js → dg_MySolution/js/utils.js
Flowline: WebResources/dist/js/utils.js → dg_mysolution/js/utils.js
Move your web resource files from the Daxif folder into WebResources/dist/, dropping the
{prefix}_{solution}/ prefix from the path:
Before: webResources/dg_MySolution/js/utils.js
After: WebResources/dist/js/utils.js
Flowline derives the Dataverse name automatically from .flowline. Run flowline push --scope webresources --dry-run
to confirm the names match before going live.
Note on case: Daxif preserves the exact folder name as the unique name prefix. Flowline lowercases the solution name. If your existing Dataverse records use mixed case (e.g.
dg_MySolution/...) and Flowline generatesdg_mysolution/..., treat this as a rename — see the web resource renaming guidance in 13-Migration-from-spkl#renaming-existing-dataverse-records.
See 08-WebResources-Project for a full guide to the TypeScript setup, Rollup build pipeline, and folder structure.
Daxif uses XrmContext (a Delegate A/S tool) for C# generation and XrmDefinitelyTyped for TypeScript:
// Daxif — F# script
Solution.GenerateCSharpContext(Env.dev, pathToXrmContext, outputDir,
solutions = ["MySolution"],
entities = ["account"; "contact"])Flowline wraps pac modelbuilder build:
flowline generate --namespace MySolution.Models --extra-tables account,contactGenerated types land in Plugins/Models/. The namespace and extra tables are saved to .flowline after the
first run — flowline generate on subsequent runs needs no arguments.
TypeScript generation (XrmDefinitelyTyped) has no Flowline equivalent. Continue using XrmDefinitelyTyped directly via
pac modelbuilderor standalone tooling for TypeScript type generation.
// Daxif — export + unpack
Solution.UpdateVersionNumber(Env.dev, solution, VersionIncrement.Revision)
Solution.Export(Env.dev, solution, outputDir, managed = false)
Solution.Extract(Path.Daxif.unmanagedSolution, customizationsFolder, xmlMappingFile, projFile)
// Flowline — one command
flowline sync --bump patch// Daxif
Solution.Pack(outputPath, customizationsFolder, xmlMappingFile, managed = false)
Solution.Import(Env.test, pathToSolutionZip, activatePluginSteps = true, publishAfterImport = true)
// Flowline
flowline deploy test
flowline deploy prodDaxif stores credentials in a local .daxif file or Windows Credential Manager and requires F# config:
// Daxif — _Config.fsx
let creds = Credentials.FromKey("UserCreds")
let dev = Environment.Create(
name = "Development",
url = "https://mydev.crm4.dynamics.com/XRMServices/2011/Organization.svc",
ap = AuthenticationProviderType.OnlineFederation,
creds = creds,
method = ConnectionType.OAuth)Flowline reuses the PAC CLI token cache:
# Interactive / MFA
pac auth create --environment https://mydev.crm4.dynamics.com
# 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.
Daxif operations are invoked by executing .fsx scripts:
fsi.exe PluginSyncDev.fsx
fsi.exe WebResourceSyncDev.fsx
fsi.exe SolutionExportDev.fsxReplace with Flowline CLI calls:
| Daxif script | Flowline command |
|---|---|
PluginSyncDev.fsx |
flowline push --scope plugins |
WorkflowSyncDev.fsx |
flowline push --scope plugins (workflow activities in same assembly) |
WebResourceSyncDev.fsx |
flowline push --scope webresources |
SolutionExportDev.fsx |
flowline sync |
SolutionImportArg.fsx |
flowline deploy <target> |
SolutionExtract.fsx |
flowline clone (first time) / flowline sync
|
SolutionPack.fsx |
Built into flowline deploy
|
GenerateCSharpContext.fsx |
flowline generate |
SolutionCreateDev.fsx |
flowline init (coming v1) |
Daxif's Extended Solution feature wraps solution import in three phases: pre-import orphan cleanup, standard import, and post-import state restoration. This solves the "additive-only import" problem inherent to Dataverse unmanaged solution import.
| Extended Solution phase | Flowline equivalent |
|---|---|
| PreImport — delete plugin steps, assemblies, web resources removed from source | Built into flowline deploy, automatic — deploy diffs the target against local Solution.xml before import and deletes what's gone. Anything blocked by a dependency retries right after import. Pass --no-delete to only report, not delete. See 07-Deploy#orphan-cleanup. |
PostImport — restore workflow statecode/statuscode
|
Not yet supported |
| PostImport — reassign workflow owners across environments | Not planned |
flowline push is not affected — direct push handles its own orphan cleanup independently of solution import.
Daxif has a full data migration module (Data.Export, Data.Import) with GUID remapping, owner
reassignment, delta export, and N:N association import. Flowline has no equivalent — it is intentionally
focused on the ALM pipeline, not data migration. Use the Configuration Migration Tool or dataverse-utils for data migration.
Daxif supports enabling or disabling individual plugin steps without a full sync:
Plugin.EnableSolutionPluginSteps(env, solution, enable = false)Flowline has no equivalent. Use Plugin Registration Tool or a direct Dataverse API call for environment-specific step activation.
Daxif generates TypeScript context via XrmDefinitelyTyped. Flowline's flowline generate produces
C# early-bound types only via pac modelbuilder build. Continue using XrmDefinitelyTyped directly
for TypeScript.