Skip to content
Platform6 flow templates, 2 installable bundles, one connector SDK

A flow engine with a memory.

Bindato runs trigger→action flows between Shopify and the systems around it, keeps a record of every step, and ships each integration as a bundle with settings the merchant confirms before anything runs.

StorefrontERP and inventory
  • Shopify
  • NetSuite
  • BrightPearl
  • Cin7
Connectors
7
Flow templates
6
Tenant-scoped tables
12

Flow engine

Trigger → action, with the failure modes decided up front.

A flow starts from a connector trigger and walks a graph of steps: actions against the other system, conditionals that branch on the record, and a split that fans a list into per-item child runs.

  • Transient errors — 429, 500, 502, 503 — retry up to 5 times with exponential backoff (250 ms doubling, capped at 30 s). Permanent errors stop the flow at that step and record why.
  • A split step fans out to at most 10,000 child runs, enqueued in chunks of 500. Over the ceiling the run fails loudly rather than processing the first ten thousand and reporting success.
  • Each step's error policy is its own: halt, retry, or continue and skip the record.
+6.1%
4,812
Runs, last 7 days
vs. previous 7 days
+0.4 pt
99.2%
Success rate
vs. previous 7 days
−3
7
Failed runs
vs. previous 7 days
1.6 sper run
Median duration

Recent runs

FlowStatus
Shopify order → NetSuite sales orderSuccess
NetSuite fulfillment → Shopify fulfillmentSuccess
NetSuite inventory → Shopify inventory levelPartial
Shopify order → BrightPearl sales order (split by location)Failed
BrightPearl goods-out shipped → Shopify fulfillment (per location)Success
Sample data. Layout and controls are the product's own.

The step vocabulary

Trigger

A connector event — webhook or poll — that starts a run with the record as its payload.

Webhook

6

triggers arrive as the event happens

Polling

5

triggers fetch on a schedule with a cursor

6

of 7 connectors can start a flow

Action

A call into the other system with a field mapping and transforms (toString, toNumber, trim…).

Conditional

Branches the run on the record: fulfil only when every line has a SKU, for example.

Split

Fans a list into per-item child runs, each with its own record and retry.

10,000

child runs per split, then the run fails loudly

500

items per enqueue chunk

Retry policy

Per step: halt, retry with backoff, or continue and skip the record.

5

retries on 429, 500, 502, 503

30 s

backoff cap, from 250 ms doubling

Reprocess

Re-enqueue a failed run, optionally with an edited payload, from the step that failed.

Bundles and the Settings tab

An integration you install, then configure — never the other way round.

A bundle is a pair of connectors and a set of flow templates. Installing it clones those flows into your account and blocks them until its Settings tab is confirmed section by section.

  • A section can be gated by a master toggle — inventory sync switched off means no warehouse mapping is demanded of you.
  • Dropdowns are backed by live reference data from your connected accounts: your warehouses, your price lists, your order statuses. Fixed lists are used only where the vocabulary genuinely is fixed.
  • Mapping tables carry per-table uniqueness rules — a payment method maps once; several carriers may feed one — and are validated on the server before a save is accepted.

Shopify + BrightPearl · Settings

5/7 confirmed
  • General
    2 settings
    Confirmed
  • Inventory
    4 settings · gated by a master toggle
    Confirmed
  • Prices
    2 settings · gated by a master toggle
    Confirmed
  • Orders
    24 settings · gated by a master toggle
    Confirmed
  • Fulfillment
    2 settings · gated by a master toggle
    Confirmed
  • Payments
    2 settings
    Needs review
  • Refunds
    6 settings · gated by a master toggle
    Needs review
Flows in this bundle stay blocked until every section is confirmed. Mapping tables are validated against the live account, not a build-time list.
Sample data. Layout and controls are the product's own.

Run records and error handling

Every run is a record you can open to the step that failed.

A run keeps a row per step with the input it received, the output it produced and how many retries it took. Errors are a queue of their own, joined to the failing step so the payload sits beside the message.

  • Resolve, ignore, or reopen an error; reopening clears the resolution rather than stamping a new one.
  • Retry re-enqueues from where the run actually began — a split child restarts at its own item, never at the top of the parent flow.
  • Edit the raw payload and reprocess when the fix is in the data, not the mapping.

Run 01J9…K3Q

Shopify order → NetSuite sales order

Failed
  1. triggertrigger

    order.created · webhook, signature verified

    12 ms
  2. has-skuconditional

    every line item carries a SKU → true

    1 ms
  3. create-sales-orderaction

    createSalesOrder · 422 after 1 attempt (not retryable)

    840 ms
Error on create-sales-order — input the step received
{ "externalId": "5827113", "currency": "GBP",
  "customerEmail": "", "lineItems": [ … 3 ] }
→ 422 customerEmail is required
  • Edit payload & retry
  • Resolve
  • Ignore
Sample data. Layout and controls are the product's own.

Connector SDK

One contract every connector implements — including Shopify.

A connector declares its auth, triggers, actions and reference data as typed metadata. The catalog, the flow builder and the connection screens work from that metadata alone, so a new system is a package, not a fork.

  • Triggers are webhooks or polling. A webhook trigger with no signature check must declare fetchOnNotify — the registry rejects it at startup otherwise — so the payload is a pointer and the record is re-fetched by id before any step acts on it.
  • Actions carry their own input and output JSON Schemas and an optional per-action rate limit — one marketplace's order read allows about a call a minute while its shipment confirm allows five a second, which is why the limit lives per action, not per connector.
  • referenceData lists — warehouses, channels, price lists — are what make Settings dropdowns real instead of hand-typed ids.

interface Connector

id, label, authType
oauth2 | api_key
one auth config per connector
triggers[]
webhook | polling
each with a JSON Schema output
actions[]
run(input, creds)
input + output schema, per-action rate limit, idempotency key
testConnection
→ ConnectionHealth
runs when a connection is saved and on schedule
refreshCredentials?
→ Partial<creds> | null
throws → connection marked needs_reauth
referenceData?
list(creds) → items
warehouses, price lists, channels for Settings dropdowns
  • verifySignatureFn
    Signed webhook

    HMAC verified against the raw body before anything is enqueued.

  • fetchOnNotify: true
    Unsigned webhook

    Payload is treated as a pointer only. The engine forces fetchOnNotify and re-reads the record by id from the source API.

  • intervalMinutes
    Polling

    A cursor-based fetchFn on an interval, for APIs with no webhooks.

Sample data. Layout and controls are the product's own.

Multi-tenant architecture

Isolation enforced by the database, checked at boot.

Every request resolves to one tenant before a query runs, and every query runs inside a transaction that carries that tenant id to Postgres, where row-level security does the filtering.

  • 12 tenant-scoped tables have RLS enabled and FORCED, each with a tenant_isolation policy, so the table owner is bound too.
  • The API refuses to start if any scoped table is missing RLS, is not forced, or has no policy — the guarantee is verified every deploy, not assumed once.
  • The app connects as its own role, never the superuser; an admin bypass cannot leak through the application path.

One request, four checks

postgres · rls
  1. 1. Session → tenantrequireAuth

    Every authenticated request resolves to one tenant id before any query runs.

  2. 2. Transaction-scoped contextwithTenantContext

    Queries run inside a transaction that sets the tenant id for the connection's lifetime, as the app role — never the table owner.

  3. 3. Row-level security on 12 tablesFORCE ROW LEVEL SECURITY

    RLS is enabled AND forced, with a tenant_isolation policy on each table. Another tenant's row is not a 403 — it does not exist to the query.

  4. 4. Boot gateassertTenantIsolation

    The API refuses to start if any scoped table is missing RLS, is not forced, or has no policy. Isolation is checked, not assumed.

Sample data. Layout and controls are the product's own.

FAQ

How the engine behaves at the edges.

Can I edit the flows a bundle installs?
Yes. Installing a bundle clones its templates into ordinary flows in your account. You can change mappings, add conditionals or steps, or turn a flow off. The bundle's Settings tab still applies to all of them.
What does the engine do when the other system is rate-limited?
A 429 is a retryable error: the step waits with exponential backoff (250 ms, 500 ms, 1 s … capped at 30 s) and tries again, up to five times. If it is still failing the run stops at that step and the error appears in the queue with the payload attached.
How does a split handle a very large batch?
Child runs are enqueued in chunks of 500 up to a ceiling of 10,000 per run. A batch larger than that fails the run with a message asking you to narrow the trigger's page size or filter — it is not truncated silently.
Can I add a connector you do not have?
The connector SDK is a TypeScript package: implement the Connector interface — auth config, triggers, actions, testConnection, optional referenceData — and the catalog, flow builder and connection screens pick it up from the metadata. Talk to us if you want a system added.
Is there an audit trail?
Connection changes, credential rotations and bundle installs are written to a tenant-scoped audit log alongside the run records, so who changed what and when is a query rather than a guess.

See one order flow end to end.

Thirty minutes with a Shopify store you control. We connect it, install a bundle, and walk the run record together.