How to migrate an Azure Logic Apps workflow yourself

Published

TLDR

Rebuild a Logic Apps data handoff from its workflow definition and delivery history. Use AI to map actions and draft the worker, then test duplicate events and API rate limits before moving the sender.

Migration workflow for Azure Logic Apps showing the existing workflow, replacement, output comparison and scheduled delivery

To migrate an Azure Logic Apps data handoff, capture the workflow definition, connections, and run history. Rebuild the trigger and action logic with a small receiver and a durable worker. Compare the receiving application's accepted records and verify replay before changing which system sends updates.

How do you retry a Logic Apps API write with no receipt?

A customer webhook was accepted but the worker lacks a downstream write receipt. Distinguish explicit throttling from an ambiguous timeout after send by checking the API outcome and stable operation key.

Recovery. Honor applicable Retry-After on a 429 within the retry budget. For an ambiguous send, recover a confirmed receipt by the original key or reconcile before retrying if the receiver lacks a safe lookup or idempotency contract.

Cutover check. Compare deployed run-history branches and mapped request fields with the shadow worker, then compare receiver effects. Require two effects in the E1/E2 example despite duplicate E1, lost response and E2 throttling. Missing-ID and stale events must cause no writes.

Trace a customer webhook through the actual action contract

This synthetic route receives a customer-change event, transforms it, and sends an upsert to a destination API. Start with one sanitized successful run and one throttled run, plus the deployed workflow definition for each. Match the redacted trigger JSON to triggerBody() references and record the output shape of each connector. A run-history screenshot alone may omit secure inputs and outputs. Ask the owner for matching shape-only values. Workflow Definition Language actions and triggers

{
  "eventId": "E1",
  "customer": {"id": "C17", "version": 2, "displayName": "Ari Lee"},
  "eventType": "customer.updated"
}
Logic Apps stageReplacement contract
Request trigger and Parse JSONCheck sender authentication, request schema and allowlisted event type. Capture sender, eventId, body_sha256, customer_id, source_version. Return a receipt only after durable intake if the sender's acknowledgment contract permits it.
Condition or SwitchRoute unsupported event types to an explicit ignored or rejected terminal outcome, as agreed with the owner. Preserve missing versus explicit null for partial-update payloads.
Compose or connector field mappingsOutput {"id":"C17","revision":2,"name":"Ari Lee"}. Keep the exact omitted fields omitted for PATCH semantics. Validate before calling the destination.
HTTP or managed connectorSend one request per stable operation key customer-upsert:sender:E1. Save response type, service receipt and timestamp. If the API does not honor a key or offer a read-back, hold uncertain sends for reconciliation.
Success/failure scopesMatch actual runAfter branches. A skip, timeout, rejection and accepted write should produce different states when the old workflow treated them differently.

Build the receiver and delivery loop

Insert a unique inbox record and the corresponding outbox item in one database transaction. If the same event ID and payload digest already exist, return the original intake ID without enqueueing again. If the digest differs, preserve the old data and write a separate conflict record. Two source events for one customer can arrive concurrently, so serialize where required or use the destination's version-aware mutation. Apply the declared source-version rule before any stale update can replace a newer value.

A worker leases a pending outbox record, uses the fixed operation key, and saves the receiver's confirmed write receipt. A 429 follows a bounded retry policy and honors an applicable Retry-After. A timeout after a request was sent is an unknown outcome until the destination confirms success or absence. Treat business validation failures as terminal and route them with stable identity and evidence. Keep intake acknowledgment separate from final application acceptance. Logic Apps retry and error handling

Exercise one callback contract and cut over

Submit E1 twice, then E2 for C18 v1 with a stub response of 429 followed by a successful receipt. Submit E3 without the customer ID and E4 for C17 v1 after E1. During E1's first send, commit in the stub and drop the response before the local worker saves it. Restart the worker and resolve E1 by the original key. Expect two destination effects, E3 rejected before sending and E4 counted as stale. A same-ID payload change should appear as a conflict while both accepted destination rows remain intact.

Place the replacement behind a separate sandbox endpoint and fake service until incoming shape, outgoing fields, action outcomes and timing match the intended legacy trace. To switch a webhook, check sender timeout and any subscription-renewal contract. Stop or reroute new old-side delivery at a recorded boundary. Finish or assign each pending record to one owner. A rollback restores the original endpoint after verifying which destination writes already landed.

Collect the trigger and its delivery rules

For a Standard logic app, the Azure portal download can include workflow.json, connections.json, host.json, and local.settings.json. Take a redacted copy and record authentication methods and connection references separately. For a Consumption workflow, capture the JSON definition in code view and list its connector connections and resource settings. The definition describes the trigger and actions. Run history can show the branch a given request took, but what is retained depends on workflow settings and mode. Microsoft's Standard artifact guide · Workflow Definition Language · Run history

For each action, document runAfter conditions, expressions, loops, scopes, and retry policies. For polling triggers, capture the cursor and source ordering. For webhooks, capture the subscription and renewal behavior. Check if any steps require human approval or wait for an external callback. Keep those stateful steps with their owner unless the replacement has an approved way to hold and resume the work.

Build the receiver and worker

For a customer update, implement a webhook receiver or a poller that writes each accepted input to a durable queue. Validate the source signature or identity at the receiver. Transform required fields in a versioned function. Use the source event ID for deduplication, and keep a separate order or version key if newer updates must supersede older ones.

Send records through a worker with a configured rate limit. Respect the destination API's retry guidance for transient errors. Record the destination receipt beside the event ID. For a response timeout after submission, look up the destination by the agreed ID before retrying if the API supports that check. Use an idempotency key only where the receiving API honors it. Route validation failures and expired retries to an exception queue for review.

Implementation runbook

Export artifacts based on hosting model

For Standard, use Overview → Download app content or take the deployed release artifact. Keep each workflow's workflow.json plus connections.json, host.json, parameter definitions and redacted app setting names. A local settings file or exported parameters may contain secrets. Keep only approved keys and shapes for analysis. Record which connectors are built-in vs Azure-managed and which identity/access policy is responsible for each. For Consumption, capture the deployed Microsoft.Logic workflow and its connection resource references. Use the workflow's portal run history in either model, or Consumption's run-list API:

az logic workflow show --name "$WORKFLOW" --resource-group "$RG" \
  --subscription "$SUBSCRIPTION" -o json > logic-workflow.json
workflow_url="https://management.azure.com/subscriptions/$SUBSCRIPTION/resourceGroups/$RG/providers/Microsoft.Logic/workflows/$WORKFLOW"
az rest --method get --url "$workflow_url/runs?api-version=2016-06-01&\$top=10" \
  -o json > logic-runs.json

The ARM run-list URL above is for public Azure's Consumption resource type. For Standard collect per-workflow history through its own portal/provider path. Availability of retained history depends on workflow mode/settings. Capture action statuses, duration, retry behavior and branch results for success, throttling, malformed input, and timeout. Obtain restricted sanitized examples when contents are protected. Standard deployment artifacts · Consumption CLI · Consumption run list

Translate the workflow contract into a receiver and worker

  1. Record whether the trigger accepts a signed webhook, polls through a managed connector, or uses time windows. Capture webhook subscription/renewal and acknowledgment deadline. For a poller identify the cursor scope, pagination and when it advances. Record trigger conditions and whether splitOn fans an incoming array into separate runs. If a timer uses Recurrence or Sliding Window, explicitly model which missed intervals are replayed. Recurrence and Sliding Window
  2. Build an action contract table with source path, expression, type, runAfter outcomes, output, timeout, retry and effect. Reproduce missing versus null and empty string with fixtures. Check ordered branches, pagination tokens, ForEach concurrency and parallel updates, scope success/failure, Until limits and connector-specific side effects. Approvals, human waits and callbacks require durable state with a signed/correlated resume path and timeout. Trigger and action schema · Error handling
  3. Authenticate inbound requests and store the event or add it to a durable queue before acknowledging receipt. Keep a unique source_id,event_id key, payload hash and per-customer source version. Identical duplicate IDs are no-ops. Same ID/different hash is an incident to review. Do not acknowledge an event if the durable write did not succeed. If the sender expects synchronous business results, design that contract separately from receipt-only acknowledgment.
  4. Transform and validate in a versioned step. Consume by customer key when order matters. Rate-limit per destination, honor a usable Retry-After header and otherwise use bounded exponential backoff with jitter for transient failures. Record accepted destination receipt before marking the event delivered. On timeout after sending, query the receiving system or use its documented idempotency key. Route ambiguous writes for manual review when it has neither. Don't treat all 4xx or all 2xx as equal. Record dead-letter reason and preserve the event ID for a controlled replay.
  5. Check received = pending + confirmed + rejected + quarantined + ignored_stale over unique events, and reconcile delivery receipts independently of queue size. Alert with event ID, action and status rather than raw customer data. At cutover stop the old sender after its last confirmed source boundary and reconcile pending/delayed actions before enabling new production delivery.

Acceptance case with retries and a stale event

Submit E1 for C17 v2 twice, E2 for C18 v1, E3 with missing customer ID, E4 for C17 v1 after E1 is confirmed. Make E1's accepted API write lose the response and E2 return 429 before success. Expected: C17 v2 once, C18 v1 once, E1 duplicate recorded, E3 invalid without HTTP call, E4 stale without overwrite. In the stub, ensure E1's original receipt can be queried after restarting the worker. E2 should respect the test Retry-After interval. Repeat with same event ID and changed payload to prove a hash conflict remains visible. At shadow comparison include actual outgoing request field values, runAfter decisions, confirmation class, and latency. Disable the new sender and reconcile exact confirmed receipts before a rollback resumes the old workflow.

Prompt for a faithful handoff

Translate these sanitized Standard/Consumption artifacts and three observed traces into a trigger and action matrix. For each action show expected JSON, absent/null handling, allowed prior outcomes, concurrency, retryable status, connector receipt and restart position. Generate a local queue/worker using only a stub API. Test duplicate and stale versions, 429 + Retry-After, timeout after acceptance, invalid data and disabled-old/enabled-new boundaries.

Run the acceptance logic locally

Run this self-contained fixture with Python 3. It uses synthetic inputs and no vendor connection. For migration signoff, repeat the same checks against a sandbox on your selected replacement.

import sqlite3

with sqlite3.connect(":memory:") as api:
    api.execute("CREATE TABLE receipt (event_id TEXT PRIMARY KEY, customer_id TEXT)")
    api_attempts = []

    def send(event_id, customer_id, fail_after_commit=False):
        api_attempts.append(event_id)
        with api:
            api.execute("INSERT INTO receipt VALUES (?, ?)", (event_id, customer_id))
        if fail_after_commit:
            raise TimeoutError("mock HTTP response lost")

    def deliver(event_id, customer_id, fail_after_commit=False):
        existing = api.execute(
            "SELECT customer_id FROM receipt WHERE event_id = ?", (event_id,)
        ).fetchone()
        if existing:
            if existing != (customer_id,):
                raise ValueError("different payload for same event ID")
            return "confirmed_existing"
        if not customer_id:
            return "missing_customer_id"
        send(event_id, customer_id, fail_after_commit=fail_after_commit)
        return "confirmed_new"

    try:
        deliver("E1", "C17", fail_after_commit=True)
    except TimeoutError:
        pass
    assert deliver("E1", "C17") == "confirmed_existing"
    assert deliver("E2", "C18") == "confirmed_new"
    assert deliver("E3", "") == "missing_customer_id"
    assert api_attempts == ["E1", "E2"]
    assert api.execute("SELECT COUNT(*) FROM receipt").fetchone() == (2,)
    try:
        deliver("E1", "C18")
    except ValueError:
        pass
    else:
        raise AssertionError("same event ID with changed payload should fail")
print("PASS: timeout reconciliation, event conflict and missing customer ID")

Ask AI to translate the workflow and exercise its failure paths

Read this redacted Logic Apps definition and draw a table of trigger, action, expression, runAfter outcome, and external effect. Call out connector behavior that needs a sandbox check. Identify any approval or callback that outlives one worker execution.
Build a worker with a fake customer API. Test duplicate event IDs, a 429 followed by success, and a request timeout after the fake API records a write. Assert that the durable receipt store has one accepted result per event. Do not use credentials or call a real endpoint.

Replay a synthetic batch and cut over

Queue event E1 twice for customer C17, E2 once for C18, and E3 without a required customer ID. Make the mock API return a 429 on E2's first attempt. Expect one accepted update for C17, one for C18 after retry, and one rejected event with no outgoing request. Also send E2 twice after a worker restart and confirm the receipt survives. If the workflow relies on version order, test an older customer change after a newer one.

Run a scheduled reconciliation of queued, accepted, and rejected events. Start the replacement in shadow mode against a stub endpoint. At cutover, pause intake or establish a precise event boundary, drain the old sender, and reconcile every in-flight event. Enable one writer to the real API. For rollback, turn off the new sender and resume from the last confirmed receipt with the same event IDs.

Need help with the migration?

Explore Azure Logic Apps migration services or browse all DIY migration guides.

AuthorsMage Team