Skip to content

12 Philosophy

Remy van Duijkeren edited this page Aug 22, 2026 · 2 revisions

Philosophy

Flowline is opinionated. Every design decision traces back to a small set of convictions about what good Dataverse developer tooling looks like. This page states them.

1. Code flows up, configuration flows down: DEV is the verification gate

The source-centric mantra, "source is always the truth, pack from local source and import", breaks down in Dataverse. Entities, forms, views, relationships and workflows cannot be authored from source files. They are created in the environment and snapshotted into source control. Pretending otherwise is the inconsistency.

Causality points one way. The git repo records what an environment contained at a moment in time. Unpacked files are downstream of an edit in the maker portal, not the substrate of authoring. Environment to source, never the reverse.

AI is starting to move authoring off the maker portal: an agent can conjure tables, columns, forms and views from a terminal. The arrow still doesn't flip. The agent mutates the environment, then you snapshot. That's a nicer front-end, not source-first authoring, which is exactly why intent belongs in source (§5). Maybe the data model is the one that switches sides soon 😉.

Flowline uses a dual-flow model instead:

  • Code assets (plugins, web resources) flow up. push deploys the locally built DLL and web resources to DEV. Source drives Dataverse for these. Plugin steps, step images and web resources missing from source are deleted on push, so the code side is genuinely source-driven.
  • Dataverse configuration (entities, forms, views, workflows, relationships) flows down. sync snapshots the DEV environment state and commits it to source control. The environment is the truth here. You cannot write entity definitions from files into Dataverse; you capture what the environment already knows.

Deploy time is where that split pays off. deploy packs from Solution/src/, which holds the DLL captured during the last sync, not the current local build. That DLL was pushed to DEV, verified there, and snapshotted back. Deploy promotes what was tested, not what was built last. If you changed code after the last push but before deploy, the tested artifact is what ships.

The source-centric counterargument is reproducible builds: given source, reconstruct the artifact. That holds in general software engineering, but in Dataverse you are already environment-dependent for configuration. Flowline's model is at least internally consistent, because everything flows through DEV. Git sits in the middle as the audit trail of verified states, not as the authoring surface for Dataverse configuration.

This is also why deploy has no equivalent of pac solution pack's --map option. The solution is already a snapshot. Forms, views and business rules exist only as exported XML, regenerable from nothing. --map says everything in that snapshot is trustworthy except the one component hardest to verify: you can diff and read a form's XML, you cannot read a compiled DLL. It swaps the one artifact nobody can eyeball for a binary compiled a second time and never executed. Carving that exception into the snapshot model is backwards, not neutral. See 08-Deploy#why-no-solution-mapping for the concrete mechanics.

2. Drift is structural, not a discipline failure

The mainstream position holds that drift is preventable: keep source control as the origin, forbid manual edits upstream, work "professionally", and you won't have drift. That position makes perfect human discipline a load-bearing component of the architecture. When drift happens anyway, and it does, because makers edit portals, admins hotfix production, flows get toggled and connection references rebind, the framework has no answer except to blame insufficient rigour.

Flowline treats drift as a structural property of a platform where authoring happens in environments, not as a failure to be scolded. The tool's job is to absorb it: refresh DEV from production, detect drift, gate deploys on it. Saying "drift is normal" sounds like admitting sloppiness, which is exactly why the position goes undefended. It shouldn't be.

This is why production is the real master branch. provision copies prod into dev, test or uat and refuses to overwrite a production environment, so the master branch is read-only by construction. provision is the branch-from-master operation, DEV is a working copy refreshable from master on demand, and pushsyncdeploy is the promote-back path. Flowline doesn't merely tolerate prod-as-master. It's the only direction the tool permits environment state to flow.

3. Version at export time, not at pack time

A solution's version is metadata inside the snapshot, exported from the environment alongside the forms and views. That leaves two moments you could assign it: at export, in the environment, the way the platform does it, or at pack time, stamped into build output on a CI runner. Flowline versions at export. sync bumps the version in DEV before pulling the snapshot down, so the committed Solution.xml is the shipped version. Both come from one snapshot, so the repo can't lie about what shipped.

Export-time is what the platform itself does. The maker portal pre-increments the version on every export. Power Platform Pipelines mints a new version at deploy, which is itself an export step. Both set the number in the environment, at the moment the artifact is produced. Flowline's sync sits squarely in that camp.

Pack-time versioning is the one that fights the grain. The version lives in several files at once (Solution.xml, the assembly, control manifests, the .dll.data.xml sidecars), so stamping it late means rewriting all of them in CI, usually by regex against a build counter. Microsoft's own reference sample does exactly this across five file kinds and still gets it wrong in three: the NuGet version pinned at 1.0.0.0, IntroducedVersion drifting, an identity bump orphaning plugin steps. An entire custom MSBuild SDK exists only because passing the version at compile time doesn't natively work. Flowline avoids all of that by never assigning the version late.

4. Unmanaged is a legitimate professional choice

This is the most opinionated stance in the tool, and it is held deliberately. The Dataverse community has long treated unmanaged-in-production as amateurism. Flowline exists partly to disagree, loudly. The problem was never the package type. It was the tooling: without proper Git tracking, orphan cleanup, DTAP gates and deployment discipline, unmanaged solutions drift and become unmanageable. With those things, they don't.

Prod-as-master (§2) is itself an argument for unmanaged. Branching from production means copying a live environment into a fresh dev environment and continuing to work there, and that only works if the components stay editable. Unmanaged components do. Managed components are locked in the target environment: copy a managed production into a new dev environment and you cannot freely edit what you find, because the authoring source lives elsewhere. Choosing managed for a customer solution forfeits the branch-from-prod workflow the whole model is built on.

Flowline's orphan cleanup, Git-tracked solutions, clean diffs and promotion gates answer every legitimate argument for mandatory managed solutions. Managed deployment is fully supported, but it is never the default posture, and it is never presented as the "serious" option for the customer's own customizations.

5. Intent lives in source; the org holds a projection

Plugin registration intent lives in committed source: [Step], [Filter], [PreImage] and [PostImage] attributes on the plugin classes, and flowline:onload / flowline:onsave / flowline:onchange / flowline:tabstatechange / flowline:onreadystatecomplete annotations beside the code they wire. The source file is the registration record. The org holds a projection of it, never the original.

The pattern underneath this is Locality of Behaviour (LoB): the behaviour of a unit of code should be obvious from reading that unit alone, not reassembled from a separate registration tool, an spkl.json or the org. A [Step] attribute sits on the plugin class it registers. A flowline:onload annotation sits in the handler it wires. You see what fires and when in the same place you see what runs, with no trip to the Plug-in Registration Tool to learn that this class is a post-create step. Flowline is a deliberate fan of LoB and applies it wherever the platform allows, so the code explains itself.

That turns a class of hazards into non-events. Delete a plugin assembly and recreate it, and every step and image is re-derived from the attributes on the next push. Nothing is orphaned because nothing was ever only-in-the-org.

It matters more with AI in the loop. The alternative is imperative registration: a human or an agent issuing commands that mutate org state. The org ends up correct, and the reasoning that produced it survives nowhere. A generated script records what was done, not what was meant, and re-running an agent is non-deterministic. You may get a different, plausible, subtly wrong set of commands, and the repo can't adjudicate which is right because it never held the intended state. Source-resident intent fixes that. The file is the desired state, reconciliation is deterministic, drift is a diff, and review is reading a declaration. The agent helps you write the attribute, but the attribute, not the transcript, is what survives and what reconciles.

So put the intent in the artifact you already version, and reconcile toward it. Anything an agent or a human had to do to make the org correct is a step you will have to re-do, re-derive or re-trust later.

6. Convention over configuration

Never stated as a slogan, but present in every feature. Plugin step behaviour is derived from class names: AccountPostCreatePlugin maps to PostOperation, Create, account, sync without any config. Web resource dependencies are inferred from base-name matching and in-source // flowline:depends annotations. No spkl.json. No dependency config file. The folder structure is prescribed and discoverable by convention.

The solution file (.sln/.slnx) is the folder config. Flowline discovers all three projects, solution package, plugins and web resources, by reading the solution file, so there's no Paths block in .flowline. No second place to state what the solution file already states, and no drift risk between the two.

The constraint that keeps this honest: convention must still be obvious to the developer. Convention without discoverability is just magic. Flowline makes its conventions visible through naming, folder structure, attribute documentation, and verbose output that shows exactly what was inferred.

7. Delegate to the platform, build only the gap

When PAC CLI appeared as Microsoft's official Power Platform CLI, the decision was clear: delegate auth, solution import/export and environment management rather than own them.

Flowline is a workflow layer, not a platform. It does not re-implement what PAC CLI already does well, and when PAC CLI improves, Flowline benefits automatically. This is a deliberate bet on staying thin and composable rather than comprehensive and self-contained, which is the right trade-off for a community tool built on top of an actively developed official CLI.

Where PAC has no answer, like attribute-driven plugin registration, a Git-based push loop, sync summaries, WebResources scaffolding and AI-native schema context, Flowline fills the gap with its own implementation.

8. Safety before action, confidence before commitment

Every destructive operation supports --dry-run. DTAP promotion gates enforce environment order and block deploys that would skip steps. Error messages include the corrective command verbatim, so you get not just what went wrong but exactly what to run next. Structured exit codes follow de facto CLI conventions (curl, git) so agents and scripts can act on them without parsing text.

The consistent goal: the developer knows exactly what will happen before it happens.

But the tool never paralyzes. Every guard has an explicit escape hatch: --force, --no-delete, --skip-dtap-check. The posture is protect by default, trust explicitly. A developer who knows what they are doing can always override. A developer who doesn't is protected from unintended consequences.

9. Respect the developer's intelligence

The tone-of-voice guide is a philosophy document disguised as a style guide: talk like a smart colleague who knows their stuff and doesn't waste your time. No preamble, no hedging, no "attempting to." Verbose mode shows exact commands to copy-paste, not summaries of what happened.

This extends to the attribute documentation. [Step] explains why each pipeline stage exists: Validation runs before the transaction opens, Pre is inside the transaction, Post is after the save but inside the same transaction, and PostAsync is after the committed transaction. The tool treats developers as capable of understanding the Dataverse execution model if it's explained well once, then gets out of the way.

Flowline doesn't hide complexity. It explains it once, then keeps it off the daily path.

10. AI-native, not AI-bolted-on

An AI agent is a first-class user of Flowline, not a bolt-on. The design question was never "can we add AI features?" but "can an agent drive this tool as confidently as a human, without extra prompting or guessing?" An agent that can't tell what happened can't be trusted to act on it, so the answer had to land in the command surface rather than beside it.

Everything else follows from that. The tool speaks in structured exit codes an agent can branch on instead of parsing prose. Errors carry the exact command to run next. The repo stays honest about its own state, regenerating DATAVERSE_CONTEXT.md and AGENTS.md on every sync rather than leaving a stale opt-in. Flowline even ships as an installable agent plugin for Claude Code and Codex, so the agent knows how to drive it before you've cloned anything.

11. Idempotency as a constraint, not an afterthought

Clone is explicitly safe to re-run on an existing project. AGENTS.md and CLAUDE.md writes both check before writing, so existing content is never overwritten. AGENTS.md mutation (schema context section) is append-only. Push converges Dataverse to source state on every run. DATAVERSE_CONTEXT.md self-heals on every sync.

Idempotency is chosen repeatedly as a design constraint across unrelated features. The belief behind it: the tool should be safe to run at any time, in any order, without requiring the developer to track what has already been done. If a command is safe to run twice, you can run it without thinking about it.

The rule underneath

One rule generates most of the above. A convention imported from a context where it was correct, applied to Dataverse where its premise doesn't hold, then defended as a principle rather than re-derived as a trade-off, is worth re-examining. "Never commit binaries", "source is the truth", "drift means you were sloppy": each was earned somewhere else and carried here without adapting it. The point isn't that abstraction or discipline is bad. It's that a premise borrowed from elsewhere has to be re-checked here before it earns the name principle. Flowline's opinions are that re-derivation, done out loud.

Clone this wiki locally