Deploying Safely
FlowForger deploys a flow by writing its definition (clientdata) to the flow's workflow record in Dataverse. This page walks through a safe round trip and explains how the CLI decides which flow to update.
The commands below use --auth, which takes the Dataverse URL from auth.resources.dataverse in flowforger.config.json and acquires a token silently after the first sign-in (see App registration). Pass --url https://contoso.crm.dynamics.com to target another environment, or --url plus --token <token> (or the DATAVERSE_TOKEN environment variable) to skip --auth.
The round trip
# 1. Pull the flow as DSL (writes My-Flow.ff.ts, with the workflowId in @Flow)
flowforger pull --name "My Flow" --auth
# 2. Edit, then check it locally
flowforger validate My-Flow.ff.ts
flowforger run My-Flow.ff.ts --in payload.json --live Get_items
# 3. See what the push would change
flowforger diff --file My-Flow.ff.ts --auth
# 4. Push, refusing to overwrite designer edits you don't have
flowforger push --file My-Flow.ff.ts --auth --expect-clean
# 5. Turn it on if it is not already
flowforger activate --id <workflowid> --auth --state 1 --status 2
Step 2 is covered in Local testing. The rest is below.
pull
flowforger pull downloads a flow and decompiles it to a .ff.ts file. The file's @Flow({...}) decorator carries the flow's workflowId, so later commands find the same flow by id, not by name.
flowforger pull --name "My Flow" --auth # ./My-Flow.ff.ts
flowforger pull --id 11111111-1111-1111-1111-111111111111 --auth --out flows/my-flow.ff.ts
flowforger pull --solution MySolution --auth --out ./flows # every flow in a solution
--solution takes the solution's unique name, not its display name. See the CLI reference for every option.
diff: what a push would change
flowforger diff --file My-Flow.ff.ts --auth
diff resolves the live flow the same way push does (see How push finds the flow), compiles the local file exactly as push would send it, and compares the two definitions action by action. Nested actions are compared by name wherever they sit, so an action moved into or out of a scope is reported as moved.
Each action is reported as one of:
| Mark | Status | Meaning |
|---|---|---|
+ | only local | The action exists in your file but not in the live flow |
- | only live | The action exists in the live flow but not in your file |
~ | changed | Both have it, but it differs in type, inputs, expression, runAfter or position (moved) |
Every differing property is printed as live → local. Trigger, parameter and flow-description differences are reported the same way.
Cosmetic differences are ignored: metadata ids, key order, whitespace and function-name casing in expressions, line endings in descriptions, and the default $connections/$authentication parameters. Connection references are compared by key only: a value difference is listed but not counted, because the values held in the cloud are what keep the deployed flow's connections attached.
Output is human-readable on a terminal and JSON when piped (--json and --pretty force either). The JSON has { workflowId, flowName, file, identical, summary, actions, triggers, parameters, flow, connectionReferences }.
| Exit code | Meaning |
|---|---|
| 0 | Identical |
| 1 | Differences found |
| 2 | Usage or resolution error (for example, no flow by that name) |
Run diff before pushing onto a flow that others may have edited in the designer, and again after a push to confirm the cloud holds what you sent.
push
flowforger push --file My-Flow.ff.ts --auth
push compiles a .ff.ts file to Logic Apps JSON (using the connection references in flowforger.config.json) and sends it. A .json file is sent as is. push updates only the definition: it never changes the flow's name, state or solution membership.
After every push the CLI prints the flow's state, for example State: Activated (statecode 1, statuscode 2) or State: Draft (...) — not running!, because a flow keeps the state it had, and a Draft flow does not run until it is activated.
Guarding against designer edits: --expect-clean
flowforger push --file My-Flow.ff.ts --auth --expect-clean
With --expect-clean, push first runs the same comparison as diff. When the live flow has actions, triggers or parameters that your file lacks (typically something added in the designer since you pulled), it refuses: nothing is written, the missing names are listed, and the exit code is 2. Pull the flow again and merge, or push without the flag to overwrite on purpose.
--expect-clean only catches additions. A changed action cannot be told apart from your own local edit, so it is not checked. Use diff for the full picture. A push that creates a new flow always passes.
Requiring an active flow: --require-active
flowforger push --file My-Flow.ff.ts --auth --require-active
With --require-active, push exits 1 when the flow is not Activated after the push. The definition is still written. A newly created flow is always Draft, so a create always fails this check. Use it in CI to catch a flow that someone turned off.
Creating flows
When no existing flow is found, push creates one:
- The new flow is Draft.
pushnever activates. - For a
.ff.tsfile, the newworkflowIdis written back into the file's@Flow({...})decorator, so the next push updates this flow by id. If the decorator cannot be edited (for example@Flow(SOME_CONSTANT)), the CLI prints the id for you to add. --solution <uniqueName>creates the flow in that solution. It is ignored, with a warning, when the push updates an existing flow.--createskips the lookup and always creates. It cannot be combined with--idor a decoratorworkflowId.--no-createnever creates: a missing flow is an error (exit 2). Use it in CI, where an accidental second flow is worse than a failed job.
# First deployment into a solution
flowforger push --file My-Flow.ff.ts --solution MySolution --auth
# CI: update only, fail if the flow is missing or not running
flowforger push --file My-Flow.ff.ts --auth --no-create --expect-clean --require-active
How push finds the flow
push, diff, runs, run-show and run --replay-run all decide which live flow a file refers to in the same order:
--id <workflowid>, if given. It wins over the decorator, with a warning when they differ.- The
workflowIdin@Flow({...}), written bypulland by the firstpushthat creates the flow. - The flow name. For a
.ff.tsfile this is the@Flowname; a.jsonfile carries no name, so pass--name <flowName>. A match is updated, and no match creates a new flow (unless--no-create).
Name lookup is the weakest of the three. Prefer an id whenever you have one:
- Ambiguous names are refused. Dataverse allows several flows with the same name. When the lookup finds more than one,
pushanddiffstop without changing anything and ask for--id. - Renaming creates a duplicate.
pushnever writes the flow's name. If you rename the flow in@Flow({ name: ... })and the file has noworkflowId, the lookup searches for the new name, misses the existing flow (which still has the old name), and creates a second flow. Keep the decorator'sworkflowIdwhen renaming, or push with--id. - Flows you cannot see are not found. A flow owned by someone else and not shared with you is invisible to the lookup, so a push creates a second flow with the same name. The create message says when no existing flow was visible to you.
activate
push keeps the flow's state, so turning a flow on or off is a separate step:
# Activate
flowforger activate --id 11111111-1111-1111-1111-111111111111 --auth --state 1 --status 2
# Turn off (back to Draft)
flowforger activate --id 11111111-1111-1111-1111-111111111111 --auth --state 0 --status 1
activate needs --id, --state and --status. With --auth, --url is optional. The state codes are 0 Draft, 1 Activated, 2 Suspended; the matching status codes are 1, 2 and 3.
See also
- Cloud runs: check a deployed flow's runs and replay them locally.
- CLI reference: every flag of
pull,push,diffandactivate.