Skip to content

External flows and hooks

Flows can reach automation you already run somewhere else, in both directions:

  • Out: a step called Start an external flow sends a request to a Power Automate flow, a Zap, a Make scenario or an n8n workflow, through a connection. It can wait for that app to post an answer back.
  • In: a flow whose trigger is Another app calls it has one or more addresses, each with its own token. Another app starts the flow by posting to one.

Both are whole-school integrations, so both need the Integrations module on. Neither works on the public demo: nothing is sent to another app there, and no address can be made.

For a flow that calls Entra ID, Google Workspace or your device management, you do not need any of this. Flows call those directly through their connectors (see building a flow).

Flows, Connections tab. The tab shows for people who hold flow.publish and integration.manage. Adding, changing or removing a connection also needs connector.manage, and asks you to confirm it is you (step-up).

A connection is the address the other app gave you for starting its flow, plus what that app may be sent. To add one, choose Add a connection:

Field What it holds
Name Something you will recognise, such as “Power Automate: staff joiners”
Address The full https:// URL the other app gave you
What it may be sent Extra kinds of data beyond ids and plain details (below)
Sign what it sends Standard Webhooks headers on every request. On by default
Extra headers Up to 10, such as an Authorization header the app asks for

The address is sealed in the vault when you save it and never shown again in full. A Power Automate sig or a Zap’s path is a credential in its own right, so the list shows only the host and the last four characters. The address never appears in run history, events, the AI ledger or the audit log.

A connection is bound to its host. You can change the address later, but only to another address on the same host. An address on a different host is a different connection: add it as one, so it gets its own review. Extra header values are sealed the same way; a saved header can be replaced by name but not read back.

Plugboard refuses an address that:

  • is not https://;
  • has a user name or password in it (put a password in a header instead);
  • is not a public internet address (loopback, private, link-local and cloud metadata addresses are all refused).

When a request is sent, a redirect is never followed, the other app has 10 seconds to answer, and the request is never retried by itself.

While you type the address, Plugboard names the platform from its host and warns you about known traps, such as a Power Automate address with no sig (set Who can trigger the flow to Anyone in Power Automate), a Logic Apps address pasted by mistake, or an Apps Script address (Apps Script cannot read headers, so add a long random token to the address and check it in the script).

Every connection may be sent ids and plain details. Three kinds of data are off until you tick them on that connection:

Choice Covers
Ticket text Subjects, descriptions and replies, which people write and may hold anything
Names and addresses People’s names and email addresses
Anything about students Values from a flow that can be about a student

These are checked twice: when a flow is published (a flow that sends more than its connection allows cannot be published) and again when the step runs, in case the connection changed since.

With Sign what it sends on, Plugboard adds webhook-id, webhook-timestamp and webhook-signature headers in the Standard Webhooks form, using a signing secret shown once when you save. An app that checks them knows the request came from your Plugboard. Turn it off only if the app refuses unknown headers.

Send a test posts a small test body ({"test": true, "from": "Plugboard", ...}) to the address and says whether the other app accepted it. Each test is recorded in the audit log. Each connection lists the published flows that use it, and a connection in use cannot be removed: change those flows first.

Add Start an external flow to any flow and choose a connection. Under What it is sent, give each value a name the other app reads it by, and fill it from the trigger or an earlier step. A value the connection may not be sent is flagged on the card at once.

The request is a POST of a JSON object made of those named values, plus a plugboard object with the run’s id and the flow’s name, so the other app can say which run it is answering about:

{ "reason": "Projector in B12", "plugboard": { "runId": "...", "flow": "AV faults to the contractor" } }

A named value called plugboard cannot replace it. It carries no person or ticket text, so it needs no data class. The request is sent with:

Header Value
Content-Type application/json
User-Agent Plugboard-Flows/1
Idempotency-Key The same on every attempt of this step, so the other app can drop a repeat
webhook-id, webhook-timestamp, webhook-signature When signing is on

Plus any extra headers you saved. Records are sent as their ids, text is cut to 4,000 characters and lists to 200 items. The body is limited to 64 KB.

Turn on Wait for its answer and list What the answer carries. Plugboard then adds a callbackUrl value to the request: an address that works for this step only. The other app posts its answer there once, as a JSON object:

Terminal window
curl -X POST "$CALLBACK_URL" \
-H "Content-Type: application/json" \
-d '{"status": "done"}'

The address:

  • works once, then refuses;
  • expires when the step gives up (Give up after, at most 7 days);
  • takes at most 64 KB, and only plain values;
  • is checked against what you said the answer carries.

The answer is marked “From another app”. It can choose a branch and appear in messages, but it can never choose who or what a later step changes. If nothing comes back in time, the step follows its If this fails setting.

Waiting needs Plugboard to know its own web address: whoever runs Plugboard sets PUBLIC_URL (see environment variables).

The step acts as the person who started the run, or otherwise as the flow within its owner’s live access. It counts toward the school’s hourly automation limit; when that is reached, the step waits 15 minutes and tries again. If the other app refuses the request (a 4xx other than 408 or 429), the step fails with that status and is not tried again by itself. A 408, 429 or 5xx is said to be a failure that may pass.

Choose Another app calls it as the trigger. Under What it sends, list the values the request carries. Anything else in the request is dropped, types are checked, and text is cut to a sensible length. Everything it sends is marked “From another app”: it can start the flow and be used in messages, but never choose who or what a step changes without a person.

Once the flow is saved, Addresses lists where it can be called. Managing addresses needs flow.publish and integration.manage, the Integrations module on, and step-up for every change.

Add an address asks which app will use it, and makes a token that starts pbh_. The token is shown once, with copy buttons and two recipes (curl and Power Automate). After that, only its first characters show. Plugboard keeps a hash, never the token.

Give each app its own address, so one can be revoked without the others.

  • Make a new token rotates it. Choose how long the old token keeps working: Stops at once, Keeps working for an hour or Keeps working for a day, so you can update the other app without missing a request.
  • Revoke stops the token at once. Anything still using it is refused.

Tick Also require a signature for senders that can sign, such as n8n or a script. The address then gets a Standard Webhooks signing secret of its own, shown once. Power Automate and Zapier cannot sign.

Terminal window
curl -X POST "https://plugboard.example.edu.au/api/v1/flow-hooks/<hookId>" \
-H "Authorization: Bearer $PLUGBOARD_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"reason": "Projector in B12"}'

The rules, in the order they are checked:

Check Refused with
The body is at most 64 KB 413
Authorization: Bearer pbh_... is present and valid for this address 401
At most 60 requests a minute for this address 429
Flows are on for the school 403
The flow is published and on (or Watching) 409
The body is a JSON object 400
An Idempotency-Key header of at most 200 characters 400
With signing: webhook-id, webhook-timestamp (within five minutes) and a matching webhook-signature 401
Every value the flow marks as required is present 400

A request that passes starts one run and answers 202 with its id:

{ "runId": "..." }

A repeat with the same Idempotency-Key within 72 hours starts nothing and answers with the first run’s id, so retries are safe. With signing on, a captured request sent again under a different key is refused by its webhook-id.

Each run another app starts writes one audit entry naming the address, never the body or the token.

Terminal window
curl "https://plugboard.example.edu.au/api/v1/flow-hooks/<hookId>/runs/<runId>" \
-H "Authorization: Bearer $PLUGBOARD_TOKEN"

It answers where the run is and nothing about what it holds:

{ "runId": "...", "status": "waiting", "finished": false }

status is one of checking, proposed, waiting, running, needs attention, done, stopped or not matched. finished is true for done, stopped and not matched. It only answers for runs that address started.