Suspend & approval / Prompts
A flow can pause mid-run and wait for a resume or cancel event before continuing. This is the mechanism behind approval steps: a step that stops the flow until a human (or another system) says it’s okay to proceed.
An approval step is just a regular script with the Suspend option turned on in its advanced settings. Once it runs, the flow halts until it’s approved — either through the resume API endpoints or the approval page — and only the people holding the secret URLs generated for that step can do it.
Suspending a flow in Orvanta
Section titled “Suspending a flow in Orvanta”Other ways to pause a flow include:
- Early stop/Break: if defined, at the end of the step, the predicate expression will be evaluated to decide if the flow should stop early.
- Sleep: if defined, at the end of the step, the flow will sleep for a number of seconds before scheduling the next job (if any; no effect if the step is the last one).
- Retry a step until it succeeds.
- Schedule the trigger of a script or flow.
The event that ends the suspension is either:
- a cancel, or
- enough approvals to satisfy the number configured on the step.
The approval step itself generates one unique URL per required approval, via orvanta.getResumeUrls() (or orvanta.get_resume_urls() in Python). From there it behaves like a webhook: the flow stays suspended until enough of those URLs have been hit with an HTTP request. Each request against one of these URLs either advances the flow toward resuming or cancels it outright.
Add approval script
Section titled “Add approval script”If only specific people should be able to resume or cancel a flow, give each of them their own URL over whatever channel makes sense — email, SMS, a chat message.
To set this up, add a step, pick Approval, and either write a new script or pick an existing one from the Orvanta Hub. Orvanta creates the step with the “Suspend” option already enabled under the “Advanced” tab.
Use orvanta.getResumeUrls() in TypeScript or orvanta.get_resume_urls() in Python from the Orvanta client to generate secret URLs.
Flow-level resume URLs (pre-approvals)
Section titled “Flow-level resume URLs (pre-approvals)”By default, resume URLs are tied to a specific step. With flowLevel: true (TypeScript) or flow_level=True (Python), you can generate resume URLs for the parent flow instead. These “pre-approvals” can be consumed by any later suspend step in the same flow.
This is useful when you want to request approval early in the flow (e.g. in the first step) and have it automatically satisfy a suspend step that comes later:
// TypeScriptconst urls = await orvanta.getResumeUrls("approver1", true) // flowLevel = true# Pythonurls = orvanta.get_resume_urls(approver="approver1", flow_level=True)When a flow-level resume is received, the approval is stored at the flow level and matched when the worker checks for resumes at any subsequent suspend step.
Number of approvals/events required for resuming a flow
Section titled “Number of approvals/events required for resuming a flow”Set how many approval events are needed before the flow resumes — anywhere from a single approver up to requiring every authorized recipient to sign off.
Note that approval steps can have the same configurations applied as regular steps (Retries, Early stop/Break, or Suspend).
Timeout
Section titled “Timeout”Set a custom timeout after which the flow will be automatically canceled if no approval is received.
Continue on disapproval/timeout
Section titled “Continue on disapproval/timeout”If set, instead of failing the flow and bubbling up the error, continue to the next step, which would allow you to put a branch one right after to handle both cases separately. If any disapproval/timeout event is received, the resume payload will be similar to every error result in Orvanta: an object containing an error field which you can use to distinguish between approvals and disapprovals/timeouts.
An approval step can carry its own schema-driven form, rendered on the approval page so whoever’s approving can fill in arguments the rest of the flow uses.
In the step’s Advanced menu, open the “Suspend/Approval” tab and enable Add a form to the approval page.
Add each field with its Name, Description, Type, Default Value, and any advanced settings — they’ll appear on the approval page in that order.
Use arguments
Section titled “Use arguments”The approval form argument values can be accessed in the subsequent step by connecting input fields to either resume["argument_name"] for a specific argument, or simply resume to obtain the complete payload.
This is a way to introduce human-in-the-loop workflows and condition branches on approval step inputs.
Prompts
Section titled “Prompts”A prompt is simply an approval step that can be self-approved. To do this, include the resume URL in the returned payload of the step. The UX will automatically adapt and show the prompt to the operator when running the flow. e.g:
TypeScript (Bun)
import * as orvanta from "orvanta-client"export async function main() { const resumeUrls = await orvanta.getResumeUrls("approver1") return { resume: resumeUrls['resume'], default_args: {}, // optional enums: {} // optional }}TypeScript (Deno)
import * as orvanta from "npm:orvanta-client@^1.9.0"export async function main() { const resumeUrls = await orvanta.getResumeUrls("approver1") return { resume: resumeUrls['resume'], default_args: {}, // optional enums: {} // optional }}Python
import orvantadef main(): urls = orvanta.get_resume_urls() return { "resume": urls["resume"], "default_args": {}, # optional "enums": {} # optional }Go
package innerimport ( orvanta "github.com/blue-code-garden/orvanta-go-client")func main() (map[string]interface{}, error) { urls, err := orvanta.GetResumeUrls("approver1") if err != nil { return nil, err } return map[string]interface{}{ "resume": urls.Resume, "default_args": make(map[string]interface{}), // optional "enums": make(map[string]interface{}), // optional }, nil}A ready-to-fork version of this prompt script is available on the Orvanta Hub in each of the four languages above.
An operator — someone who only has “Viewer” access to the flow’s folder — will see the prompt pop up automatically the moment they run the flow.
Default args
Section titled “Default args”Return a default_args object from the step, keyed by form field name, to pre-fill the approval form with values instead of leaving it blank:
//this assumes the Form tab has a string field named "foo" and a checkbox named "bar"import * as orvanta from 'npm:orvanta-client@^1.9.0';export async function main() { // if no argument is passed, and the caller is logged in, it defaults to their username const resumeUrls = await orvanta.getResumeUrls('approver1'); // send resumeUrls to whoever should approve, or see the Prompt section above return { default_args: { foo: 'foo', bar: true } };}This pattern is also available as a ready-made script on the Orvanta Hub.
Dynamic enums
Section titled “Dynamic enums”Return an enums object, keyed by form field name, to populate that field’s choices at runtime instead of hardcoding them on the form:
//this assumes the Form tab has a string field named "foo"import * as orvanta from 'npm:orvanta-client@^1.9.0';export async function main() { // if no argument is passed, and the caller is logged in, it defaults to their username const resumeUrls = await orvanta.getResumeUrls('approver1'); // send resumeUrls to whoever should approve, or see the Prompt section above return { enums: { foo: ['choice1', 'choice2'] } };}This pattern is also available as a ready-made script on the Orvanta Hub.
This is what lets a form field’s options come from a live source rather than a fixed list: compute the choices as the first step of a flow, and the approval form that follows reflects whatever that step returned.
Below is the flow YAML used for this example:
summary: ""value: modules: - id: a value: type: rawscript content: >- import * as orvantaClient from "orvanta-client" export async function main() { // Constant array, but could come from dynamic source const customers: string[] = [ "New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", "San Antonio", "San Diego", "Dallas", "San Jose" ]; const resumeUrls = await orvantaClient.getResumeUrls("approver1"); // Remove duplicates and sort the customers array in alphabetical order const sortedCustomers = Array.from(new Set(customers)).sort(); return { resume: resumeUrls['resume'], enums: { "Customers to send to": sortedCustomers }, default_args: { "Customers to send to": sortedCustomers } } } language: bun input_transforms: {} is_trigger: false continue_on_error: false suspend: required_events: 1 timeout: 1800 hide_cancel: false resume_form: schema: properties: Customers to send to: items: type: string type: array description: "" required: [] order: - Customers to send to summary: Approval step with dynamic enum - id: b summary: Use the selected arguments value: type: rawscript content: |- # import orvanta def main(x): return x language: python3 input_transforms: x: type: javascript expr: resume["Customers to send to"] is_trigger: false same_worker: falseschema: $schema: https://json-schema.org/draft/2020-12/schema properties: {} required: [] type: objectDescription
Section titled “Description”The approval step can carry a description shown on the approval page, using the full range of rich display rendering — not just plain text or markdown.
import * as orvanta from "orvanta-client@^1.9.0"export async function main(approver?: string) { const urls = await orvanta.getResumeUrls(approver) // send the urls to their intended recipients // if the resumeUrls are part of the response, they will be available to anyone with access // to the run page and allowed to approve from there, even non-owners of the flow // self-approval can be disabled in the suspend options return { ...urls, default_args: {}, enums: {}, description: { render_all: [ { markdown: "# Delivery pending approval" }, { map: { lat: 52.37, lon: 4.90, markers: [{lat: 52.37, lon: 4.90, title: "Warehouse — Amsterdam"}]} }, "Confirm the shipment before it leaves the warehouse." ] } // supports every rich-display format: plain strings, markdown, html, // images, tables, maps, render_all, etc. // https://docs.orvanta.cloud/concepts/rich-display-rendering/ }}Hide cancel button on approval page
Section titled “Hide cancel button on approval page”By enabling this option, the cancel button will not be displayed on the approval page, to force more complex patterns using forms with enums processed in later steps.
Alternatively, adding the cancel URL as a result of the step will also render a cancel button, providing the operator with an option to cancel the step. e.g:
import * as orvanta from "orvanta-client"export async function main() { const urls = await orvanta.getResumeUrls("approver1") return { resume: urls['resume'], cancel: urls['cancel'], }}Permissions
Section titled “Permissions”Require approvers to be logged in
Section titled “Require approvers to be logged in”By enabling this option, only users logged in to Orvanta can approve the step.
Disable self-approval
Section titled “Disable self-approval”The user who triggered the flow will not be allowed to approve it. This restriction is enforced both on the approval page and on the flow resume API endpoint, preventing flow owners from bypassing approval requirements programmatically.
Require approvers to be members of a group
Section titled “Require approvers to be members of a group”By enabling this option, only logged-in users who are members of the specified group can approve the step.
You can also dynamically set the group by connecting it to another node’s output.
Get the users who approved the flow
Section titled “Get the users who approved the flow”The input approvers is an array of the users who approved the flow.
To get the list of users, just have the step after the approval step return the approvers key. For example, by taking an input connected to the approvers key.
The step could be as simple as:
export async function main(list_of_approvers) { return list_of_approvers}With input list_of_approvers taking the JavaScript expression approvers.
Slack approval step
Section titled “Slack approval step”Both the Python and TypeScript Orvanta clients ship a helper for requesting an interactive approval directly in Slack — a message with approve/reject buttons that resolves the step without anyone needing to open the Orvanta UI.
Two ready-made Hub scripts wrap this helper, one per language:
- Python: Request Interactive Slack Approval
- TypeScript: Request Interactive Slack Approval
If the approval step also has a form, that form renders as a modal attached to the Slack message.
Both scripts call the same client function underneath:
Python
orvanta.request_interactive_slack_approval( slack_resource_path="/u/alice/slack_approval_resource", channel_id="admins-slack-channel", message="Please approve this request", approver="approver123", default_args_json={"key1": "value1", "key2": 42}, dynamic_enums_json={"foo": ["choice1", "choice2"], "bar": ["optionA", "optionB"]},)Bun
await orvanta.requestInteractiveSlackApproval({ slackResourcePath: "/u/alice/slack_approval_resource", channelId: "admins-slack-channel", message: "Please approve this request", approver: "approver123", defaultArgsJson: { key1: "value1", key2: 42 }, dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] }, });dynamic_enums sets an enum form field’s options at runtime, and default_args pre-fills a field’s default value — same as the plain approval form covered above.
To require approval from more than one channel, call the helper once per channel instead of relying on a single invocation:
Python
import orvanta
def main(): # Send approval request to customers orvanta.request_interactive_slack_approval( 'u/alice/slack_approval_resource', 'customers', ) # Send approval request to admins orvanta.request_interactive_slack_approval( 'u/alice/slack_approval_resource', 'admins', )Bun
import * as orvanta from "orvanta-client"export async function main() { await orvanta.requestInteractiveSlackApproval({ slackResourcePath: "/u/alice/slack_approval_resource", channelId: "customers" }) await orvanta.requestInteractiveSlackApproval({ slackResourcePath: "/u/alice/slack_approval_resource", channelId: "admins" })}Microsoft Teams approval step
Section titled “Microsoft Teams approval step”The TypeScript Orvanta client also exposes helpers for requesting approval through Microsoft Teams. The interactive variant posts a Teams message that can be approved or rejected without leaving Teams; the basic variant just posts a link back to the approval page in the Orvanta UI.
Two Hub scripts cover both cases:
- Request Interactive Teams Approval
- Request Basic Teams Approval
As with Slack, a form defined on the approval step renders as a modal inside the Teams message.
Both scripts call the same client function underneath:
TypeScript Interactive
await orvanta.requestInteractiveTeamsApproval({ teamName: "Orvanta", channelName: "General", message: "Please approve this request", approver: "approver123", defaultArgsJson: { key1: "value1", key2: 42 }, dynamicEnumsJson: { foo: ["choice1", "choice2"], bar: ["optionA", "optionB"] }, });TypeScript Basic
const card_block = { "type": "message", "attachments": [ { "contentType": "application/vnd.microsoft.card.adaptive", "content": { "type": "AdaptiveCard", "$schema": "https://adaptivecards.io/schemas/adaptive-card.json", "version": "1.6", "body": [ ... // card body ], }, } ], "conversation": {"id": `${conversation_id}`}, } await orvanta.TeamsService.sendMessageToConversation({ requestBody: { conversation_id, text: "A workflow has been suspended and is waiting for approval!", card_block } })dynamic_enums and default_args work the same way here as they do for the Slack helper above. Requesting approval from several Teams channels at once follows the same pattern too — one call per channel.
Tutorial: a Slack approval step conditioning flow branches
Section titled “Tutorial: a Slack approval step conditioning flow branches”Whatever an approver enters into the approval form can drive which branch a flow takes next — a common shape for human-in-the-loop workflows.
Here’s a minimal version of that pattern. The flow starts from a manual trigger with two string inputs: “User email” and “Order number”.
The first step is the Hub’s Request Interactive Slack Approval script, configured with:
- “slackResourcePath”: the path to your Slack connection.
- “channel”: the Slack channel to post the approval message in.
- “text”:
Refund request by _${flow_input["User email"]}_ on order ${flow_input["Order number"]}.
That lets whoever’s on call approve or reject the refund straight from Slack, no need to open Orvanta at all.
Under that step’s Advanced settings, on the “Suspend/Approval” tab, add form fields for “Action” and “Message” — those become the approval page (and the Slack modal, since the two stay in sync).
Once approved, the payload carries two keys forward: resume["Action"] and resume["Message"], both reachable from resume, the full resume payload.
Feed those into a branch’s predicate expression to route the flow based on what the approver chose.
What each branch actually does is up to you — this tutorial only cares about the approval step. A simple version might send an email via Gmail for one branch and post to a Slack channel for the other.
Automated trigger version
Section titled “Automated trigger version”Instead of a manual trigger, the Mailchimp Mandrill integration can kick off this same flow whenever a matching email arrives.
A working copy of this flow is available to fork on the Orvanta Hub.