Send low-stock alerts to the ERP

Turn a stock row running low into a purchase requisition in your ERP with an Integration Studio workflow, decide whether the platform or the ERP raises reorders, and book the delivery back in without counting it twice.

A SKU at one location runs low, and the next hourly scan announces it to a workflow that asks your ERP for a requisition, with the stock row's id as the reference. When the supplier delivers, your ERP side books the goods into stock under the ERP's goods-receipt number. This guide builds both halves: an Integration Studio workflow for the signal going out, and one guarded API call for the delivery coming back. The platform sends the signal at least once, not exactly once, and normally one per row and UTC day (two scans either side of midnight UTC can announce the same row twice), so the guide shows where your side has to absorb a repeat.

This is for integrators connecting an ERP to a shop whose stock the Inventories app keeps (its reference page is Stock). You need a tenant with the Inventories core app and Integration Studio installed, access to Cockpit (the admin interface at app.revenexx.com) to change the app's settings and build a workflow, and an ERP with a requisition or purchase-request API that a workflow can reach over HTTPS. The Inventories app's scope is inventories.* (see which scope covers which app). New to the platform? Prerequisites covers Cockpit, your tenant slug, installing an app and a test tenant. A stock row is one item at one location, and a requisition is whatever your ERP calls a request to buy more.

Integration Studio is experimental and its schemas are drafts, so expect a workflow built today to need a migration later. Where this guide says something is not stated, not promised or not alerted, that was checked in October 2026 and can change (as of 2026-10).

Before you start: two keys

Both keys are created in Cockpit under API keys (see create an API key), with custom scopes. inventories.* would cover the whole app, writes included, so the keys below name single operations instead. Narrow scopes limit which operations a key can call, not which rows, so keep the second key on the ERP side.

KeyScopesFor
Setup key, used from your own machineinventories.stock.list, inventories.locations.list, inventories.stock.update, inventories.reorder_alerts, inventories.reorder_scanSteps 2 and 3, and the test at the end of step 6
ERP side's key, used by whatever notices a delivery in the ERPinventories.movements.list, inventories.receiveStep 7

Step 3 also reads the event catalog, which has no documented scope name. A development key with * covers that read, as which scope covers which app says, so run it with a development key or skip it and type the topic from step 4. The workflow in steps 4 to 6 calls only your ERP and needs no platform key. Running step 7 from a workflow does: its Generic API Call node takes a Revenexx API credential holding the ERP side's key (see Reaching Revenue Cloud).

REVENEXX_TENANT is your tenant slug, the value you send as X-Revenexx-Tenant (see the glossary):

Environment
export REVENEXX_TENANT="<TENANT_SLUG>"
export REVENEXX_API_KEY="rvxk_..."      # the setup key
export REVENEXX_ERP_API_KEY="rvxk_..."  # the ERP side's key, for step 7

The shell examples use jq to pick values out of the answers. Where your stock numbers come from in the first place is a different question, answered in Choose which system owns each entity.

How the loop runs

The loop
Platform (Inventories app)     Integration Studio            Your ERP
──────────────────────────     ──────────────────────        ───────────────────
hourly scan: available
<= reorder point
  └─ stock_level.low ────────▶ event trigger starts a run
                               └─ HTTP Request ──────────▶  requisition, keyed on
                                                            the stock row's id

          ...the supplier delivers...

POST /v1/inventories/receive ◀──────────────────────────── ERP side calls the API
  └─ +on_hand, one inbound movement                         with its goods-receipt number

Who is responsible for what:

Responsible for
The platformThe hourly scan, the event, a fixed id per row and day, and the append-only movements ledger. No delivery time is promised: the scan is hourly and the event's delivery adds time
The workflowCarrying the event to the ERP, retries, and what happens on a failure
Your ERPAnswering a repeated request with the open requisition it already holds, and closing a requisition when a goods receipt fills it
YouThe reorder points, the failure wiring, and the guard against booking a delivery twice

1. Decide who raises the reorder

The Inventories app can watch reorder points itself, and an ERP usually does the same job. Running both gives a buyer two requisitions for one shortage. Pick one.

Your situationreorder_alert_enabledWho raises the reorder
The platform holds the stock, and purchasing happens in the ERPOn (the default)The platform signals, the ERP orders. This guide
The ERP knows the stock and the reorder logic, and a sync copies its numbers onto the platformOffThe ERP. Skip steps 2 to 6. Step 7 applies only if deliveries are booked with receive and the sync does not already carry them
A sync is any integration that writes the ERP's stock figures onto the platform.on_hand cannot be set directly: a PUT on a stock row drops it, and there is no route that sets an absolute value, so a sync has to book movements, with adjust (a signed difference) or receive. If the sync books a delivery's goods, never also call receive for that delivery, or they count twice. Choose one path for deliveries: the sync, or step 7.

reorder_alert_enabled is a setting of the Inventories app. Open Apps → Installed Apps → Inventories → Settings in Cockpit, where it appears as Reorder alerts. It is market-scoped, meaning one tenant-wide value plus an optional override per market: in the Global baseline (no market selected) it is the tenant-wide value, and with a market selected in Cockpit's top bar it saves an override for that market. If you use markets, set it in the Global baseline and leave the markets inheriting: how the hourly scan resolves a per-market override is not stated anywhere you can read. The effective value for a market is readable from the API, and Settings shows how.

Off silences stock_level.low and the worklist, and nothing else.stock_level.out_of_stock and stock_level.back_in_stock still fire whatever the setting says, so a workflow subscribed to them keeps receiving events from a tenant that turned reorder alerts off.

2. Give the stock rows a reorder point

A row alerts when its available (on_hand − reserved) is at or below its reorder_point. The point sits on the row, so it is per item and per location. A row that carries none falls back to the reorder_point_default setting, shown as Default reorder point on the same Inventories Settings screen as Reorder alerts in step 1. A point of 0, or none at all, never alerts, and reorder_point_default starts at 0, so a tenant that has set nothing receives nothing.

Setting a point can spend that row's event for the day. A row whose available is already at or below its point is announced at the next hourly scan, whether or not a workflow exists yet, and that is its one event for the UTC day. Default reorder point works the same way for every row without a point of its own. So in this step set points only on rows whose available is still above the new point, and leave the row you will test with until step 6.

Find the row's id first. The list filters by exact sku:

Find the stock row
curl -s "https://api.revenexx.com/v1/inventories/stock?sku=ACME-4711-BLK" \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  | jq '{filter, rows: [.items[] | {id, location_id, on_hand, reserved, reorder_point}]}'

A stock row names its location by location_id, while the event and the receive in step 7 use the location's code. List the locations once to map one to the other. Every tenant starts with a location coded main:

Map location ids to codes
curl -s https://api.revenexx.com/v1/inventories/locations \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  | jq '[.items[] | {id, code, name}]'

Read filter in the stock answer. A query parameter the list does not know is dropped, and the list then comes back unfiltered, so filter is how you tell a typo from "nothing matched". Then set the point on the row you want, with the id from the answer. Only the fields you send change:

Set a reorder point
curl -s -X PUT "https://api.revenexx.com/v1/inventories/stock/<STOCK_LEVEL_ID>" \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reorder_point":25}'

Check what the scan will find before you build anything on it. The worklist is computed on read, so it is what the next scan sees:

Read the worklist
curl -s https://api.revenexx.com/v1/inventories/reorder-alerts \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  | jq '{enabled, reorder_point_default, rows: [.alerts[] | {sku, location_id, available, reorder_point, shortfall}]}'

It answers enabled: false and an empty list when the setting is off. Rows come worst shortfall first. A row at a location you disabled still alerts.

3. Look up the topic to subscribe to

The event's name is stock_level.low. A workflow trigger matches the topic, which puts the vendor and the app in front of the name. Read it from your tenant's event catalog instead of typing it:

Read the topic
curl -s https://api.revenexx.com/v1/events/catalog \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  | jq -r '.emits[] | select(.app == "inventories" and (.name | IN("stock_level.low", "stock_level.out_of_stock", "stock_level.back_in_stock"))) | .topic'
Response
revenexx.inventories.stock_level.low
revenexx.inventories.stock_level.out_of_stock
revenexx.inventories.stock_level.back_in_stock

The catalog lists the Inventories app only while it is installed and active on the tenant, and it lists its other events too, such as stock_level.updated, so the filter above keeps just the three that matter here. The three events are not alike, and this guide builds on the first:

EventFires whenCarries beyond the common fields
stock_level.lowThe hourly scan finds a row at or below its reorder pointreorder_point, reorder_point_source (row or default), shortfall
stock_level.out_of_stockA booking takes available from above 0 to 0 or belowprevious_available
stock_level.back_in_stockA booking takes available from 0 or below to above 0previous_available

All three carry stock_level_id, sku, product_id (either of the two may be empty, depending on how the row is keyed), location_code, on_hand, reserved, available and detected_at. out_of_stock and back_in_stock are about whether the item can be sold, not about how much is left: with backorders on, available goes negative, and going deeper into backorder fires nothing. A receive that creates the stock row, such as the first delivery of a new SKU, fires neither.

4. Start the workflow on the event

In Cockpit, open Integration Studio → Workflows → New and add an Event trigger. The trigger's field is labelled Subject; enter the topic from step 3:

Subject
revenexx.inventories.stock_level.low

A subject segment could not carry an underscore before 2026-09-29, so if a workflow with this exact subject never starts, use revenexx.inventories.*.low instead: the * stands for exactly one segment (here stock_level), and the pattern matches the same topic. See Event triggers.

Click Save, and leave the switch in the editor's header on Paused. A new workflow is paused, and nothing fires until you activate it. You activate it in step 6, once the ERP node and the failure path are wired: the hourly scan runs by itself, so an active workflow with nothing behind its trigger would start empty runs and use up each low row's event for the UTC day. Each save queues a build, and runs are refused until it is ready.

Once the workflow is active (step 6), each event starts one run. The event's own fields sit under data in the trigger payload:

ExpressionValue
${{ trigger.payload.data.stock_level_id }}The stock row's id. The reference for everything below
${{ trigger.payload.data.sku }}The item
${{ trigger.payload.data.location_code }}The location, as a code such as main
${{ trigger.payload.data.available }}What can still be promised
${{ trigger.payload.data.reorder_point }}The point it fell to or below
${{ trigger.payload.data.shortfall }}How far below it is
${{ trigger.payload.id }}The event's id, not the row's

A stock_level_id is the row, and ${{ trigger.payload.id }} is the event. Reaching for the wrong one gives every later call an id no stock row has, and because both ids are in the payload, nothing refuses the expression: the mistake shows only in what those later calls answer.

A manual scan spends the day's event for every row that is low, so wait for the test at the end of step 6.

How often a row alerts

stock_level.low is level-based, not a crossing: the scan asks every hour whether the row is low. A row that stays low is announced at most once per UTC day, and the next day brings a new event for the same row if it is still low. The platform gives each row and day one fixed event id, so a repeat within the day is absorbed.

SituationWhat reaches the workflow
A row stays low for a weekOne event per day, seven runs
A row recovers and drops again the same dayNothing new. The day's event already went out
Scans just before and just after midnight UTCThe same row can be announced twice, an hour apart
One event delivered twiceOne run: the trigger drops a repeat of the same event id

Event delivery is at-least-once, and the trigger's own de-duplication does not help with the daily repeat, because each day's event has its own id. The workflow passes every repeat on with the same stock_level_id, and the ERP's duplicate check in step 5 is what recognises it. detected_at on this event is the day the scan ran, at 00:00:00Z, not the moment the row crossed the point.

5. Ask the ERP for the requisition, once per shortage

Add an HTTP Request node that sends the requisition to your ERP's API, and set it like this:

SettingValue
MethodThe one your ERP's requisition API takes, usually POST. The node calls any standard method
URLYour ERP's requisition endpoint. It accepts expressions, so a workflow variable such as ${{ vars.erpUrl }} lets you point it at a test system first
Headers and bodyBoth accept expressions. Add the headers your ERP needs, a Content-Type among them. The docs do not list the node's body modes, so read the request in the first run's step to see what was sent
CredentialAn HTTP Bearer Token credential, which you create and test under Integration Studio → Credentials and then pick on the node. See Credentials
TimeouttimeoutSeconds under Execution settings in the node's Settings tab: 1 to 7200 seconds, 10 minutes by default. See Failure handling

Bearer is the only HTTP credential type the platform's own node package ships, and the docs name no other for HTTP Request. If your ERP wants basic auth, an API key or OAuth, the docs do not say how to authenticate this node, so ask your ERP team whether it can issue a bearer token. They also do not say where the workflow's requests come from, or whether an ERP behind a firewall can be reached: ask your ERP's network team what it would have to allow, and ask support how the platform reaches it.

The body is whatever your ERP expects, built with the expression picker. For a JSON API, the mapping looks like this:

Request body
{
  "reference": "${{ trigger.payload.data.stock_level_id }}",
  "article": "${{ trigger.payload.data.sku }}",
  "plant": "${{ trigger.payload.data.location_code }}",
  "minimum_quantity": "${{ trigger.payload.data.shortfall }}"
}
From the eventInto the ERP
stock_level_idThe external reference, and the key for the ERP's duplicate check
skuThe article
location_codeThe receiving plant or warehouse
shortfallThe minimum quantity. Your purchasing rules decide the real amount

The platform guarantees two things: the stock_level_id stays the same for the row for its whole life, and a row that is still low is announced again every UTC day. When a requisition counts as open is your ERP's decision. This guide's advice, not platform behaviour: a requisition is open from the moment it is created until your ERP books the goods receipt that fills it. Then one rule: answer a repeat of the same stock_level_id while a requisition is open with success and the id of the open requisition, instead of creating a second one. That covers the daily repeat, a retried node and a replayed run. Once the receipt is booked the requisition is closed, so a later shortage on the same row opens a new one under the same stock_level_id.

A partial delivery that leaves the row below its point is announced again the next UTC day against the same id. Under the rule above the requisition is still open, so the repeat gets it back and the remainder is not ordered twice. If your ERP closes a requisition on the first receipt instead, that repeat opens a new one for the remainder, so decide on purpose which you want. We recommend closing only when the requisition is filled.

If your ERP can only refuse a duplicate, ask first. Add a node before the request that looks up an open requisition for this stock_level_id: a second HTTP Request node against your ERP, with the same bearer credential. Continue only when none exists: wire the If condition's true port (no open requisition) to the request that creates it, and leave the false port unwired or end it on a No Operation node. Conditions are not expressions: set the condition's field to the part of the lookup's answer that says whether a requisition exists, such as a count or an empty list, then an operator and a value. Prefer a lookup that answers 200 with a count or an empty list. How the HTTP Request node treats a 404 or another non-2xx answer is not documented, and a lookup that answers 404 for "none found" may end on the node's error port instead of reaching the condition. Try what your ERP answers for "none found", and what the node does with it, before you rely on it.

The request node also makes three attempts in total by default, even with no retry configured, and a request that timed out may still have been booked on the ERP side. Until the duplicate check is in place, set the node's retry to one attempt (maxAttempts: 1) with Max attempts under Retry on failure in the node's Settings tab. See Failure handling.

Closing a requisition is your ERP's job: close it when a goods receipt fills it, not on the first partial receipt. That is the same receipt step 7 reports to the platform. Do not use stock_level.back_in_stock to close one: it fires when available rises above 0, not when it rises above the reorder point, and a row can be back in stock and still below its point.

6. Make a failure visible

An event-driven workflow fails with nobody watching. On the HTTP Request node, open the Settings tab and set On error to Route to error output (onError: route). The default, Fail the run, fails the run and never reaches the error port. Then wire that error port to something your team reads, such as a Send Email node, and end that branch with a Stop and Error node that names the row:

Stop and Error message
Requisition failed for ${{ trigger.payload.data.sku }} at ${{ trigger.payload.data.location_code }}, see the run's steps

Without it the run fails and nobody is told: there is no alert on a failed run, so check Integration Studio → Runs filtered to failed as part of watching the integration.

An event that arrived while the workflow was being saved does not become a run. It lands under Missed runs with its payload, and Replay starts a run from it. The next day's event covers the same row if it is still low, so for a daily signal a missed event costs at most a day. Missed runs is documented only for firings that land while a build is not ready; the docs do not say that an event arriving while the workflow is Paused is kept, so do not count on it. See Missed runs.

Run it once

Run this once steps 5 and 6 are built, and with the HTTP Request node pointed at an ERP you can afford a requisition in, a test system if it has one. Then activate the workflow: flip the switch in the editor's header from Paused to Active. Saving a workflow with unfilled required settings, such as a half-configured node, asks first and saves it paused, so check the switch after the save. See Validation and the activation gate.

Read the worklist from step 2 first: the scan announces every row on it, not one row you pick, and each announcement is that row's one event for the UTC day. If no row on it is one you want to test with, give a test row a point at or above its current available with the PUT from step 2 (a point of 0 never alerts), then read the worklist again.

Run the scan by hand with the setup key. It runs the same scan the hourly schedule does and answers with the rows it announced:

Run the scan now
curl -s -X POST https://api.revenexx.com/v1/inventories/reorder-alerts/scan \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  | jq '{enabled, scanned, emitted}'

emitted lists the rows the scan announced, which is not the same as runs: a second scan on the same day lists the same rows again but announces nothing new, so no new runs start. Open Integration Studio → Runs and look for one run per row on the worklist. A row already announced earlier the same UTC day is not announced again, so a row that alerted this morning has had its event for today: a row that has not alerted today is the one to test with, or wait for tomorrow's scan. Afterwards set the test row's point back to what it was, with the same PUT.

7. Book the delivery back in

The supplier delivers, and your ERP books a goods receipt. Report it to the platform with a receive call. The simplest way is for the ERP to make that call itself, with the ERP side's key in REVENEXX_ERP_API_KEY and no workflow in between:

Book a delivery
curl -s -X POST https://api.revenexx.com/v1/inventories/receive \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_ERP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "location_code": "main",
        "reason": "WE-4500091234",
        "items": [ { "sku": "ACME-4711-BLK", "quantity": 120 } ]
      }'

location_code is the location's code from step 2, the same value the event carried as location_code and step 5 sent to the ERP as the plant. items takes 1 to 200 entries, each a sku or a product_id plus a positive quantity, or the same fields inline for one item. Without location_code the default_location_code setting decides, and a code no location carries answers 400 instead of booking somewhere else. A tenant whose movement_reason_required is all also needs reason. The answer is 201 with the touched rows at their new balance, and one inbound movement is written per item. The row is created if the item is new.

To run this half as a workflow instead, for example because the ERP can only call a URL, build a second workflow, separate from steps 4 to 6, with its own trigger: a Webhook trigger that your ERP calls when it books the receipt. The address is issued per trigger and shown on it in Cockpit. A webhook trigger has no authentication setting today, so treat the address as a secret, and do not assume that only your ERP can call it: a call to this one books stock. How an endpoint is protected depends on how it was provisioned for your tenant, so ask support what protects yours before you hand the address to the ERP. See Webhook triggers. The low-stock event does not start this workflow.

In a workflow the values come from the ERP's call, which the Webhook trigger hands over as ${{ trigger.payload.body }}. If the ERP posts one line per call, the mapping into the receive body looks like this, with whatever names your ERP sends after body.:

receive bodyFrom the ERP's call
location_code${{ trigger.payload.body.plant }}
reason${{ trigger.payload.body.goods_receipt_number }}
items[0].sku${{ trigger.payload.body.article }}
items[0].quantity${{ trigger.payload.body.quantity }} (a number or a numeric string such as "120")

A receipt with several lines is one call and not one transaction. The whole body is checked before anything is written, so a bad line answers 400 and books nothing. The lines are then booked one at a time with no transaction around them, so a failure part-way leaves the earlier lines booked, and a retry of the whole call books them again. Two lines with the same sku in one call are two bookings, each with its own inbound movement.

A receive is not idempotent. A retried call books the goods a second time, because the call carries no reference the platform could recognise a repeat by. Anything that retries it, including a workflow node with the default of three attempts in total, turns one lost answer into double stock.

Guard against a double booking

Make the repeat detectable yourself. Put the ERP's goods-receipt document number in reason, one value per delivery event, and look for it before you book. Use the number of the receipt the ERP booked for this delivery, not the purchase-order number: a second, partial delivery on the same order has its own goods receipt, and under the order number it would find the first booking and be skipped without an error. The platform stores reason as plain text, trimmed of surrounding spaces, and does not check it for uniqueness; the app sets no length or character limit. Keeping it unique per delivery is yours to do. Filter by the same item key you book with: a movement stores the sku only when the receive named the item by sku, so a delivery booked by product_id is found with product_id=<ID>, and a sku filter would answer 0 and let a second booking through.

Was this delivery booked?
curl -s "https://api.revenexx.com/v1/inventories/movements?type=inbound&sku=ACME-4711-BLK&reason=WE-4500091234" \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_ERP_API_KEY" \
  | jq '{filter, booked: .page.total}'

Book only when booked is 0, and read filter to confirm all three filters were understood. They are exact matches, so the same goods-receipt number has to go in as reason every time. This check looks at one item. If you book a receipt line by line, a second line with the same SKU finds the first line's movement and is skipped, so use the receipt-wide check below for a receipt with several lines. If the check and the booking run in a workflow:

  1. A Generic API Call node, with the Revenexx API credential from the start of this guide, asks the question above.
  2. An If condition continues only when the count is 0. Its field is the answer's page.total.
  3. The receive is a second Generic API Call node. It runs with Max attempts at 1 and On error at Route to error output, both in its Settings tab. A failure then lands on its error port, which you wire to the same alert branch as in step 6, and the next run's check finds out whether the call got through. A failed run does not run again by itself: Retry it from Integration Studio → Runs (see Runs), or let the ERP call the trigger again. Either way, item 1 asks first.

The check and the booking are two calls, so this is safe for one delivery handled by one run at a time. Two runs for the same receipt at once can both see 0, so serialise a receipt on the ERP side.

For a receipt with several lines, drop the sku filter and ask about the receipt: ?type=inbound&reason=WE-4500091234. page.total is then the number of lines already booked, one inbound movement per line, repeated SKUs included. Book the whole receipt when it is 0. If it is above 0 and below the number of lines, the earlier call failed part-way: read the movements, compare their sku or product_id and quantity with the receipt's lines, and book only the lines they do not show.

If stock does end up doubled, find the two inbound movements with the same reason, and correct it with one adjustment: POST /v1/inventories/stock/<STOCK_LEVEL_ID>/adjust with a negative quantity and a reason, using a key with the inventories.stock.adjust scope (neither key above has it). See the booking routes. The ledger is append-only, so a booking is never deleted.

Troubleshooting

SymptomLikely cause
The workflow never startsThe workflow is still Paused, or the subject does not match the topic from step 3 segment for segment. Nothing warns you. Try the * form from step 4
No event for a row that is lowIts reorder_point is 0 or unset and reorder_point_default is 0, or the setting is off, or the row already alerted today. Check the worklist from step 2
The ERP gets the same requisition dailyThe ERP does not answer a repeated stock_level_id with the open requisition, or it closed the requisition before the shortage was filled (step 5)
Events keep coming after you turned reorder alerts offThe setting silences low only. out_of_stock and back_in_stock are separate
An event fires after the stock is replenishedThe scan saw the row before your receive, or the receive went to another location than the row's
Stock doubled after a deliveryA retried receive, see step 7
receive answers 400A quantity of 0 or less, more than 200 items, a location_code no location carries, or no reason when the tenant needs one
403 on a callThe key lacks the scope for that operation (the tables above list them)

Where to go next

  • Stock — the movements ledger, reservations and the other booking routes.
  • Hand an order to the ERP — the same workflow shape for the way out of an order, and the ERP's calls back.
  • Choose an event surface — when a workflow is the right consumer of an event and when a webhook is.
  • Failure handling — retries, error routing and timeouts for the steps above.
Was this page helpful?