# Builder Agent
Source: https://docs.cloudcruise.com/concepts/builder-agent
Generates a workflow through chat
Our browser agent is made up of two cooperating agents: The builder and the maintenance agent.
Together, these agents enable a robust automation lifecycle: from rapid creation to continuous adaptation.
By generating enough context at build time, our agents execute faster, have lower cost, and improved reliability compared to traditional browser-use frameworks that use LLMs at every step at runtime.
| Agent | Responsibility |
| - | - |
| **Builder Agent** | Creates the workflow via chat with a human or an AI. |
| **Maintenance Agent** | Monitors live runs and repairs the workflow whenever the target site changes. |
## Builder Agent
### Purpose
Turn a business process into structured workflow context for our browser agent.
### How it works
1. **Kick‑off** – Open a new workflow in the dashboard and describe the task. You can paste a detailed SOP or simply type a high‑level goal like “Log in and fill out this tracking form.”
2. **Schema discovery** – Optionally provide JSON schemas for input variables and expected output. Skip this, and the agent infers them automatically.
3. **Interactive build** – The agent launches a live browser preview, navigates the target site, and asks clarifying questions only when needed (e.g., login credentials, ambiguous clicks).
4. **Credential capture** – Any secrets you share are stored in your [vault](/concepts/vault) and reused securely in subsequent runs.
5. **Graph generation** – Each confirmed action converts into a node‑edge graph you can inspect, edit and version.
6. **Save & run** – Once the agent completed the graph generation, hit **Save** to get a dedicated API endpoint for triggering this workflow.
## Why we generate workflows instead of using AI for every step
Traditional “agentic” tools keep an LLM in the loop for every click, form fill, and page transition. CloudCruise compiles the logic up front and reuses it. The payoff shows up in three dimensions:
| Dimension | Runtime LLM at every step | CloudCruise compile‑once approach |
| - | - | - |
| **Speed** | Each action incurs network latency plus token inference time, leading to multi‑minute runs. | Graph executes at native browser speed; only API calls hit the network. |
| **Cost** | Pay per token, per step—cost scales linearly with run length. | One‑time build tokens are amortized over thousands of executions; marginal cost is near‑zero. |
| **Reliability** | High variance: LLM may hallucinate selectors or loop indefinitely; reproducibility is low. | Fully deterministic; every run follows the same graph. Maintenance Agent handles drift proactively. |
# Citrix Virtual Desktops
Source: https://docs.cloudcruise.com/concepts/citrix
Automate workflows inside Citrix VDI environments via HTML5 browser access
Many enterprise applications run exclusively inside Citrix virtual desktops—locked behind VPNs, proprietary networks, or compliance boundaries. CloudCruise can automate these applications by connecting to Citrix through its HTML5 browser interface, treating the remote desktop as just another web page.
## How It Works
Citrix Virtual Apps and Desktops (formerly XenApp/XenDesktop) offer an HTML5-based web client that streams the remote session directly into a browser tab. CloudCruise connects to this stream and interacts with the virtualized desktop using the same click, type, and extract primitives you'd use on any web application.
Because the entire session lives inside a standard browser, CloudCruise can:
* **Navigate** to your organization's Citrix portal
* **Authenticate** with stored credentials and handle 2FA
* **Launch** specific virtual desktops or published applications
* **Interact** with applications inside the virtual session
* **Extract** data from screens using AI vision
## Prerequisites
| Requirement | Details |
| - | - |
| Citrix StoreFront or Workspace URL | Obtain from your IT team (e.g., `https://citrix.yourcompany.com` or `https://yourcompany.cloud.com`) |
| Login credentials | Username and password with access to the required virtual desktops |
| HTML5 receiver enabled | Your Citrix admin must enable HTML5 Receiver / Workspace for Web |
| MFA configuration | If required, set up in your [Vault](/concepts/vault) entry |
Ask your Citrix administrator to confirm that HTML5 Receiver is enabled. Some organizations disable browser access and require the native Citrix Workspace app.
## Setting Up a Citrix Workflow
### Step 1: Store Credentials in the Vault
Create a [Vault](/concepts/vault) entry with your Citrix credentials:
```json theme={null}
{
"username": "domain\\your_username",
"password": "your_password"
}
```
If your organization uses 2FA, add the appropriate configuration:
```json theme={null}
{
"username": "domain\\your_username",
"password": "your_password",
"tfa_method": "AUTHENTICATOR",
"tfa_secret": "JBSWY3DPEHPK3PXP"
}
```
### Step 2: Build the Connection Workflow
When building your workflow with the [Builder Agent](/concepts/builder-agent), the typical flow is:
1. **Navigate** to your Citrix portal URL
2. **Authenticate** with vault credentials
3. **Handle 2FA** if prompted
4. **Select** the target desktop or application from the launcher
5. **Choose HTML5 mode** when prompted (select "Use light version" or "Use browser")
6. **Wait** for the virtual desktop to fully load
7. **Perform** your target automation inside the session
### Step 3: Interact with the Virtual Desktop
Once inside the Citrix session, CloudCruise uses **AI (Screenshot)** execution mode to interact with the remote desktop. Since the desktop is rendered as a canvas element, standard DOM selectors don't work—the agent uses vision-based interaction instead.
Set execution type to **AI (Screenshot)** for all click and input actions inside the Citrix session. This allows the agent to visually locate buttons, text fields, and other UI elements within the remote desktop.
Example node for clicking inside a Citrix session:
```json theme={null}
{
"id": "click-citrix-app",
"name": "Open application in Citrix",
"action": "CLICK",
"parameters": {
"execution": "LLM_VISION",
"element_description": "The icon labeled 'Patient Records' on the desktop"
}
}
```
Example node for typing text:
```json theme={null}
{
"id": "type-patient-name",
"name": "Enter patient name in search field",
"action": "INPUT_TEXT",
"parameters": {
"execution": "LLM_VISION",
"element_description": "The search input field in the top navigation bar",
"text": "{{context.inputs.patient_name}}"
}
}
```
## Best Practices
| Practice | Rationale |
| - | - |
| Leverage session persistence | Enable **Cookies** and **Local Storage** in your Vault entry to reuse sessions |
| Use descriptive element names | Helps the vision model locate targets accurately |
| Avoid window overlaps | Keep the target application in the foreground |
## File Transfers
File downloads and uploads through Citrix require additional consideration:
### Downloads from Virtual Desktop
1. Files download to the **virtual desktop's local storage** first
2. Use Citrix's file transfer feature to move files to the browser
3. CloudCruise can then capture the file using the [File Download](/concepts/workflow-dsl/file-download) node
### Uploads to Virtual Desktop
1. Use the [File Upload](/concepts/workflow-dsl/file-upload) node to upload to the Citrix client
2. Navigate to the file in the virtual session using the Citrix file browser
### Interaction Failures
| Symptom | Solution |
| - | - |
| Clicks miss their target | Add delays for UI stabilization; improve action prompts; Use AI traces for each run and the `reasoning` output to improve action prompts |
| Text input appears garbled | Slow down typing speed; some Citrix clients need character-by-character input |
| Actions go to wrong window | Ensure the target window is focused before interacting |
## Limitations
Compared to automating native web applications, Citrix workflows have some constraints:
| Limitation | Impact |
| - | - |
| No DOM access | Must use AI vision for all interactions |
| Higher latency | Each action has network round-trip overhead as well as addded AI inference latency |
| Session dependencies | Virtual desktop must remain stable throughout the run |
## Need Help?
Citrix environments vary significantly between organizations. If you encounter configuration issues or need assistance building a Citrix workflow, contact us at **[founders@cloudcruise.com](mailto:founders@cloudcruise.com)**.
# Payload Limits
Source: https://docs.cloudcruise.com/concepts/limits
Request body and field size limits, and the errors they return
## Request body limits
| Endpoint | Limit |
| - | - |
| `POST /run` | 25 MB |
| `POST /workflows/{id}/validate-input` | 25 MB |
| `POST /workflow-builder/agent/session/from-workflow`, `POST /workflow-builder/agent/session/from-failed-run` | 25 MB |
| `POST /workflows`, `PUT /workflows/{id}`, `POST /workflows/upsert`, `POST /workflows/import` | 5 MB |
| All other endpoints | 1 MB |
A larger body returns `413 Payload Too Large`:
```json theme={null}
{
"statusCode": 413,
"code": "PAYLOAD_TOO_LARGE",
"message": "Request body is too large",
"limit_bytes": 26214400,
"received_bytes": 27262976
}
```
`received_bytes` is present when the request sets `Content-Length`.
## Field limits
| Field | Limit | Code |
| - | - | - |
| Workflow `nodes` | 2 MB | `WORKFLOW_NODES_TOO_LARGE` |
| Workflow `edges` | 512 KB | `WORKFLOW_EDGES_TOO_LARGE` |
| Workflow `input_schema` | 256 KB | `INPUT_SCHEMA_TOO_LARGE` |
| Workflow `output_schema` | 128 KB | `OUTPUT_SCHEMA_TOO_LARGE` |
| Workflow `vault_schema` | 32 KB | `VAULT_SCHEMA_TOO_LARGE` |
| Workflow `name` | 256 characters | `WORKFLOW_NAME_TOO_LONG` |
| Workflow `description` | 4,096 characters | `WORKFLOW_DESCRIPTION_TOO_LONG` |
| Workflow `version_note` | 2,048 characters | `VERSION_NOTE_TOO_LONG` |
| Run `dry_run.add_to_output` | 4 KB | `DRY_RUN_OUTPUT_TOO_LARGE` |
Byte limits apply to the stored size, which the database may compress. A JSON value can therefore exceed its limit in raw size and still be accepted.
A value over its limit returns `400 Bad Request`:
```json theme={null}
{
"statusCode": 400,
"code": "WORKFLOW_NODES_TOO_LARGE",
"message": "nodes exceeds the 2097152-byte limit",
"field": "nodes",
"limit_bytes": 2097152
}
```
| Field | Description |
| - | - |
| `code` | Stable identifier of the limit that was hit |
| `field` | Request field that exceeded the limit |
| `limit_bytes` / `limit_chars` | The limit, in bytes or characters |
| `actual_chars` | Length of the submitted value. Present for `version_note` |
A request that also fails other validation returns `code: "VALIDATION_FAILED"` and lists every error in `message`.
# Maintenance Agent
Source: https://docs.cloudcruise.com/concepts/maintenance-agent
Understand, recover, and repair workflow failures
The Maintenance Agent is CloudCruise's failure-diagnosis and recovery layer. When a workflow action fails, it explains what happened and chooses the safest next step: recover in place, request input, retry later, notify your team, or hand the failure to the Builder Agent.
You can control recovery behavior at the workspace and workflow levels under **Error Handling > Maintenance Agent Settings**. Workspace settings determine which workflow-level recovery controls are available in the dashboard.
## What you can do
| Customer outcome | What CloudCruise provides |
| - | - |
| Understand what failed | A category, sub-type, plain-language explanation, failing step, and captured run context |
| Recover automatically | Popup, XPath, page-loading, password, 2FA, outage, and logged-out-session recovery |
| Ask for help safely | Webhooks that request corrected data instead of making consequential choices |
| Repair the workflow | Manual XPath correction, Edit Workflow, and Fix with Builder Agent |
| Control recovery | Workspace and workflow settings, retry limits, popup XPaths, and approved error-code actions |
| See what the agent accomplished | Recovered status, recovered error IDs, requeue events, screenshots, video context, and eligible monthly recaps |
| Improve recovery over time | Node Description Enrichment, workflow descriptions, and approved error-code suggestions |
## Understand what failed
The Maintenance Agent evaluates more than the raw error message. Depending on the failure, it can use the error screenshot, visible page state, DOM and selector signals, current URL, failing step, recent actions, context from successful runs, and patterns from previous failed runs. It also learns from recovery outcomes and can apply site-specific knowledge about recurring timing, selector, popup, validation, login, and outage behavior.
The resulting category, sub-type, and explanation are available in the [Retrieve Run Results](/run-api/retrieve-run-results) response as `llm_error_category`, `llm_error_sub_type`, and `llm_error_description`. Terminal failures also include them in the [`execution.failed`](/concepts/webhooks) webhook.
### Error-classification matrix
| Category | Sub-types | Customer meaning |
| - | - | - |
| `SERVICE_UNAVAILABLE` | `APPLICATION_ERROR`, `SERVICE_RETIRED`, `MAINTENANCE_DOWNTIME`, `SILENT_ERROR` | The external application errored, is under maintenance, was retired, or is silently broken |
| `AUTHENTICATION_ERROR` | `INVALID_CREDENTIALS`, `MFA_CODE_REJECTED`, `ACCESS_DENIED`, `TOO_MANY_ATTEMPTS` | Login, verification, credentials, or account access failed |
| `PAGE_STILL_LOADING` | `IN_PROGRESS`, `PREMATURE_ACTION` | The page had not finished loading, or the action fired before its target was ready |
| `UNEXPECTED_UI_STATE` | `ACTION_REQUIRED`, `DECISION_REQUIRED` | A popup, dialog, overlay, or banner blocked progress — dismissible, or needing a consequential decision |
| `PASSWORD_UPDATE_REQUIRED` | `DISMISSIBLE`, `NON_DISMISSIBLE` | The site presented an optional or mandatory password-change screen |
| `INCORRECT_INPUTS` | `FIELD_VALIDATION_ERROR`, `NO_MATCHING_OPTION`, `MULTIPLE_MATCHING_RESULTS_FOUND` | An input was rejected by validation, had no matching option, or matched several ambiguous results |
| `XPATH_INCORRECT` | — | A selector found no element, multiple elements, or the wrong element |
Historical runs may retain legacy category names (e.g. `ACCOUNT_LOGGED_OUT`, `INCORRECT_FORM_INPUTS`), which now map to `AUTHENTICATION_ERROR` and `INCORRECT_INPUTS`. Recovery routing continues to accept supported legacy values, so existing error-code mappings do not need to be rewritten.
## Recover automatically
### Popup Recovery
Popup Recovery handles unexpected dialogs, overlays, banners, and other blocking UI states without making consequential choices on your behalf.
* For an `ACTION_REQUIRED` blocker, the agent confirms that it is visible and uses a targeted action to close it. If the blocker is no longer present, the recovery action safely does nothing.
* For a `DECISION_REQUIRED` state, the agent requests input instead of choosing blindly. The request includes failure context and a screenshot when available.
Enable **Popup Handling** in Maintenance Agent Settings. You can also configure known dismiss controls with `popup_xpaths`; see [Workflow Settings](/concepts/workflow-dsl/workflow-settings#popup-handling).
### XPath Recovery
XPath Recovery handles eligible Click and Input Text steps when a selector no longer finds the intended element, matches multiple elements, or resolves to the wrong element shape.
The agent uses the current page, failed selector, match count, failure subtype, and step description to perform the intended action with AI and resume after the failed step. Recovery is bounded and stops safely when the required page context is unavailable or recent attempts for the same step have repeatedly failed.
Enable **XPath Recovery** in Maintenance Agent Settings. This runtime recovery does not guarantee that the workflow's stored selector will be permanently replaced.
### Page Loading Recovery
Page Loading Recovery handles actions that run before the relevant page or component is ready. It applies to Click, Input Text, Input Select, Scroll, and Extract Datamodel nodes.
| Page state | Initial temporary wait |
| - | - |
| Component, partial-page, or lazy-content loading | 10 seconds |
| Spinner, empty content area, navigation, or other broad loading state | 15 seconds |
On the first eligible failure for a step, the agent injects a temporary Delay and retries the action. Attempts are tracked per step and session to prevent loops. If the retry still fails, the agent can retrace to an earlier eligible step when enough run context is available. A full requeue is the final fallback when Service Unavailable Recovery is enabled.
Page Loading Recovery is controlled by **Action Timing Recovery** (`enable_action_timing_recovery`). It is enabled by default but can be disabled at the workspace or workflow level.
### Password Update Recovery
Password Update Recovery handles password-expiration and mandatory-rotation screens while keeping the vault credential current.
1. The agent distinguishes an optional prompt from a mandatory password-change form.
2. Optional prompts can be dismissed.
3. For a mandatory update, the agent reads the current vault credential and visible password requirements.
4. It generates a compatible password, fills the required fields, and submits the form.
5. After success, it updates the vault and sends an [`execution.password_updated`](/concepts/webhooks) webhook.
6. The workflow resumes using the updated credential.
Enable **Password Update Recovery** in Maintenance Agent Settings or set `enable_password_update_recovery` through the [Workflow API](/workflow-api/update-workflow).
Automatic rotation requires exactly one vault credential on the workflow. When multiple credentials are present, CloudCruise does not guess which credential to rotate and sends a notification instead. Provider-backed credentials are written back to the connected provider; see the [1Password integration](/integrations/1password#password-rotation-write-back).
### 2FA Setup Recovery
2FA Setup Recovery can complete a required enrollment flow for the vault credential associated with the current site.
| Scenario | Recovery action |
| - | - |
| Method selection | Selects an available method, preferring authenticator, then email, then SMS |
| Authenticator setup | Reveals and validates the manual setup secret, stores it encrypted in the vault, and enters a current TOTP code |
| Existing authenticator challenge | Enters a code using the secret captured during the recovery flow |
| SMS verification | Requests and enters the verification code through the configured [CC 2FA Proxy](/concepts/twofactor#2-cc-2fa-proxy-hands-off-sms--email--magic-link) |
| Email verification | Requests and enters the verification code through the CC 2FA Proxy |
| Optional setup prompt | Dismisses the prompt or proceeds into setup when the screen requires that path |
Enable **TFA Setup Recovery** in Maintenance Agent Settings or set `enable_tfa_setup_recovery` through the [Workflow API](/workflow-api/update-workflow).
### Service and Logged-Out Session Recovery
Service Unavailable Recovery requeues transient external failures such as application errors, HTTP errors, timeouts, and active maintenance windows. A permanently retired service is reported but not retried.
Logged-out sessions use the same retry setting. When a site expires or invalidates a session during a workflow, CloudCruise notifies your configured channels and can requeue with stored cookies and browser state cleared, giving the next attempt a clean authentication path.
A silent login failure may also receive one bounded retry when retry recovery is enabled and the workflow has succeeded recently.
The retry delay follows `10 minutes × 2^n`, where `n` begins at 0, with ±20% jitter:
| Retry | Approximate delay |
| - | - |
| 1 | \~10 minutes |
| 2 | \~20 minutes |
| 3 | \~40 minutes |
| 4 | \~1.3 hours |
| 5 | \~2.7 hours |
| 6 | \~5.3 hours |
| 7 | \~10.7 hours |
| 8 | \~21 hours |
| 9 | \~1.8 days |
| 10 | \~3.5 days |
Enable **Service Unavailable Recovery** and set **Maximum Error Recovery Attempts** from 0 to 10. While waiting, the run status is `execution.requeued`. The corresponding webhook includes the retry attempt and scheduled execution time.
## Ask for help safely
### Incorrect Form Input Recovery
Incorrect Form Input Recovery lets your external agent or automation correct data without restarting the entire run.
1. The failure is classified as `INCORRECT_INPUTS`, or a matched error code uses **Request New Input**.
2. CloudCruise sends an [`execution.input_required`](/concepts/webhooks) webhook containing the session ID, current inputs, error details, timeout, and screenshot when available.
3. Your system calls `POST /run/{session_id}/new_input_variables` with corrected values.
4. CloudCruise resumes from the relevant step in the executed workflow path.
5. If no response arrives before the configured timeout, the run follows the normal failure path.
Enable **Incorrect Form Input Recovery** in Maintenance Agent Settings, or configure an approved error code with **Request New Input**. The workspace timeout can be set from 5 to 300 seconds. An active wait can be stopped with [Interrupt Run](/run-api/interrupt-run).
See [Submit New Input Variables](/run-api/submit-new-input-variables) for the complete request format.
### Consequential popup decisions
Decision-required popups use the same input-required lifecycle instead of being dismissed automatically. The webhook provides the current inputs, failure context, timeout, and a screenshot when available so your system can provide corrected values before the workflow resumes.
## Repair the workflow
Automatic recovery keeps the current run moving. When a workflow needs a permanent change, the run details page provides repair tools for members with the appropriate permissions:
* **Manual XPath correction** lets you review a suggested selector against the captured DOM and save a corrected workflow version.
* **Edit Workflow** opens the workflow directly for manual changes.
* **Fix with Builder Agent** starts an interactive repair session with the failed workflow, failing step, and run inputs already selected.
* **Adopt fix with Builder Agent** may appear to workspace admins for eligible repeated popup recoveries. Dismissible popups require a validated reusable selector; decision-required popups are handed to the Builder Agent without pinning a choice automatically.
## Control recovery behavior
Workspace settings establish the recovery controls available in the dashboard. Workflow settings let you tailor those controls for an individual workflow.
| Setting | API field | Default | Customer outcome |
| - | - | - | - |
| Popup Handling | `enable_popup_handling` | `true` | Dismiss non-consequential popup blockers or request a decision |
| Action Timing Recovery | `enable_action_timing_recovery` | `true` | Wait and retry eligible actions when a page is still loading |
| XPath Recovery | `enable_xpath_recovery` | `true` | Use AI to complete eligible Click and Input Text selector failures |
| Error Code Generation | `enable_error_code_generation` | `true` | Suggest error codes for eligible unmatched failures |
| Node Description Enrichment | `enable_node_description_enrichment` | `true` | Generate diagnostic step descriptions from successful runs |
| Service Unavailable Recovery | `enable_service_unavailable_recovery` | `true` | Requeue transient outages and logged-out sessions |
| Incorrect Form Input Recovery | `enable_incorrect_form_input_recovery` | `false` | Request corrected data through a webhook |
| Password Update Recovery | `enable_password_update_recovery` | `false` | Rotate one compatible vault credential and resume |
| TFA Setup Recovery | `enable_tfa_setup_recovery` | `false` | Complete supported 2FA enrollment flows |
See [Workflow Settings](/concepts/workflow-dsl/workflow-settings#recovery-toggles) for the complete API-field reference.
## See what the agent accomplished
Run details distinguish failures that were recovered from failures that still need intervention. Recovered rows show the affected step and can link to the corresponding point in the run video when a recording is available.
The [Retrieve Run Results](/run-api/retrieve-run-results) response includes:
* `was_recovered` — whether at least one Maintenance Agent recovery succeeded
* `recovered_error_ids` — the recovered error IDs, correlated with the response's `errors` array
Lifecycle webhooks expose the same operational journey:
| Event | Meaning |
| - | - |
| `execution.requeued` | The run is waiting for another attempt |
| `execution.input_required` | CloudCruise is waiting for corrected data or a popup decision |
| `execution.password_updated` | Password rotation succeeded and the credential was updated |
| `execution.failed` | The run reached its terminal failure path |
Eligible organizations may also receive a monthly recap summarizing recovered popup blockers, selector failures, outages, slow-loading pages, password updates, and 2FA setup. Delivery depends on organization eligibility and minimum recovery volume.
## Improve recovery over time
### Node Description Enrichment
Clear step intent helps the Maintenance Agent distinguish selector drift, timing problems, missing prerequisites, and unexpected page states. Node Description Enrichment creates that context for workflows that have it enabled.
1. The enrichment cycle prioritizes enabled workflows with frequent failures and limited diagnostic context.
2. It uses screenshots from a recent successful, non-retried run.
3. Eligible steps with missing or short descriptions receive a 2–3 sentence diagnostic description and structured intent.
4. Descriptions are saved in a new workflow version and used during future diagnoses.
5. A missing or short workflow description can also be generated from the enriched steps.
New workflows have **Node Description Enrichment** enabled by default; toggle it in Maintenance Agent Settings. Descriptions under 30 characters are eligible. Control-flow nodes such as Start, End, Delay, Loop, and Bool Condition are skipped.
### Error code suggestions
For authentication, password-update, and incorrect-input failures, the Maintenance Agent can compare its diagnosis with your approved workspace error codes. When no code matches and Error Code Generation is enabled, it creates a pending suggestion for review instead of silently approving a new code.
After approval under **Error Handling > Error Codes**, future matching failures can return the code in run results and `execution.failed` webhooks. Approved error codes can also define actions such as fail and notify, retry, pause, cancel queued runs using the same authentication, or request corrected input.
# Rate Limits
Source: https://docs.cloudcruise.com/concepts/rate-limits
API rate limits and best practices
All CloudCruise API endpoints are subject to the following global rate limits:
| Time Window | Max Requests |
| - | - |
| 1 second | 500 |
| 15 seconds | 1,000 |
| 60 seconds | 2,000 |
Rate limits are tracked per API key (by workspace), and unauthenticated requests fall back to per-IP tracking. If multiple time windows are exceeded simultaneously, the most restrictive limit applies.
## 429 Response
When you exceed a rate limit, you'll receive a `429 Too Many Requests` response:
```json theme={null}
{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Too Many Requests",
"limit": 500,
"remaining": 0,
"retryAfterSeconds": 1
}
```
| Field | Description |
| - | - |
| `limit` | Maximum requests allowed in the violated time window |
| `remaining` | Requests remaining in the violated window. This is `0` when the request is throttled |
| `retryAfterSeconds` | Seconds to wait before retrying |
The response also includes rate-limit headers: `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. Use `Retry-After` or `retryAfterSeconds` to decide when to retry.
## Best Practices
* **Use webhooks instead of polling.** [Webhooks](/concepts/webhooks) notify your server immediately when a run completes, eliminating the need for polling the [run results](/run-api/retrieve-run-results) endpoint.
* **Use our SDKs.** The [TypeScript](/sdk/js/overview) and [Python](/sdk/python/overview) SDKs use server-sent events for real-time updates, avoiding polling entirely.
* **Implement exponential backoff.** If you receive a `429`, wait for `retryAfterSeconds` before retrying and increase the delay on subsequent failures.
# SAML Single Sign-On
Source: https://docs.cloudcruise.com/concepts/saml-sso
Let your team sign in to CloudCruise with your identity provider over SAML 2.0
CloudCruise supports **SAML 2.0 Single Sign-On (SSO)**, so your team signs in to the dashboard with your own identity provider (IdP) instead of a CloudCruise password. You can also **require** SSO for everyone on your email domain, centralizing access in your IdP and turning off password sign-in.
SAML SSO is part of the **Enterprise plan** and is configured per **organization** by an organization **owner**. If you don't see the Authentication settings, [contact CloudCruise](mailto:founders@cloudcruise.com) to enable it.
## How it works
SSO is set up in two steps, both from [Organization Settings → Authentication](https://app.cloudcruise.com/organizations/authentication):
1. **Verify your domain** — prove you own the email domain your team uses (e.g. `acme.com`) by adding a DNS TXT record.
2. **Attach a SAML configuration** — paste your IdP's metadata (Entity ID, SSO URL, signing certificate) onto the verified domain.
Once configured, anyone with an email on that domain can authenticate with SSO. Access to an organization or workspace is still controlled by CloudCruise membership and invites. Optionally, turn on **Require SSO** to block password sign-in and sign-up for the domain entirely.
Any SAML 2.0–compliant identity provider works, including:
* **Okta**
* **Microsoft Entra ID** (Azure AD)
* **Google Workspace**
* **OneLogin**
* **Ping Identity**
* **JumpCloud**
* …and any other SAML 2.0 IdP
## Before you start
You'll be moving values back and forth between your IdP and CloudCruise. From CloudCruise, your IdP needs these **Service Provider (SP)** details:
| Setting in your IdP | Value |
| - | - |
| **ACS URL** (a.k.a. Reply URL, Single sign-on URL, Assertion Consumer Service) | `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/acs` |
| **SP Entity ID** (a.k.a. Audience URI, Identifier) | `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/metadata` |
| **SP Metadata URL** (if your IdP supports importing it) | `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/metadata` |
| **Name ID format** | Email address (`urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) |
These Service Provider URLs are the same for every CloudCruise customer — they're CloudCruise's hosted SAML endpoint. Point your IdP's ACS and Audience at them, and send the user's **email address as the SAML Name ID**.
From your IdP, CloudCruise needs three values (covered in [Step 2](#step-2-add-your-saml-configuration)):
* **IdP Entity ID** — your IdP's SAML issuer / metadata identifier.
* **IdP SSO URL** — the IdP login endpoint (must be **HTTPS**).
* **IdP signing certificate** — the IdP's x509 public certificate, in PEM format.
***
## Step 1: Verify your domain
Go to your organization's [**Settings → Authentication**](https://app.cloudcruise.com/organizations/authentication) page. (This link resolves to your active organization automatically.)
Under **Verified domains**, enter the domain your team signs in with (for example `acme.com`) and click **Add domain**. A verification dialog opens automatically.
Add the following record at your DNS provider (the dialog shows the exact values with copy buttons):
| Field | Value |
| - | - |
| **Type** | `TXT` |
| **Name / Host** | `_cloudcruise-sso-verification.acme.com` |
| **Value** | `cc-sso-verify=` |
The token is generated per domain — copy it from the dialog rather than from this page.
Click **Start verification**. CloudCruise checks DNS every 15 seconds for up to 15 minutes (you can also click **Check now**). Once the record is found, the domain flips to **Verified**.
DNS changes can take anywhere from a minute to a few hours to propagate. If verification keeps failing, the dialog tells you exactly what it found at the hostname — e.g. a stale `cc-sso-verify=` record or no record at all.
A domain can only be **verified by one organization**. Multiple organizations can have a *pending* claim on the same domain, but only the first to verify it via DNS takes ownership.
***
## Step 2: Add your SAML configuration
First create a SAML application in your IdP using the [Service Provider details above](#before-you-start), then bring the IdP's values back into CloudCruise.
In your identity provider, create a new SAML 2.0 application and set:
* **ACS URL** → `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/acs`
* **Audience / SP Entity ID** → `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/metadata`
* **Name ID** → the user's **email address**, format `emailAddress`
Assign the app to the users or groups who should be allowed to authenticate with CloudCruise. They still need CloudCruise organization and workspace membership to access your team's workspaces.
From the IdP's SAML metadata (or app settings), collect:
* **IdP Entity ID** (e.g. `https://idp.acme.com/metadata`)
* **IdP SSO URL** — the HTTPS sign-on endpoint (e.g. `https://idp.acme.com/sso`)
* **Signing certificate** in PEM form, including the `-----BEGIN CERTIFICATE-----` / `-----END CERTIFICATE-----` lines
Back on the **Authentication** page, under **SAML configuration**:
1. Choose the **verified domain** from Step 1.
2. Paste the **IdP Entity ID**, **IdP SSO URL**, and **IdP Certificate (PEM)**.
3. Click **Create configuration**.
CloudCruise validates the values and registers the provider with its hosted SAML endpoint.
Open the [login page](https://app.cloudcruise.com/login), choose **Sign in with SSO**, and enter a work email on the configured domain. You should be redirected to your IdP and, after authenticating, returned to CloudCruise signed in.
The **IdP SSO URL must use HTTPS**, and the certificate must be a valid PEM-encoded x509 certificate (starting with `-----BEGIN CERTIFICATE-----`). The dashboard rejects values that don't meet these requirements.
***
## Signing in directly from your IdP (optional)
Most identity providers offer a dashboard where users can click an app tile to launch straight into a connected app — Okta calls this an **app tile**, Entra ID and OneLogin call it a **bookmark application**. If you try pointing that tile directly at CloudCruise's SAML app, users are redirected to the CloudCruise login page instead of being signed in.
This is a limitation of the **IdP-initiated SAML** flow itself, not a CloudCruise-specific bug: an app tile POSTs an unsolicited SAML assertion straight to CloudCruise's SAML endpoint, with none of the request state CloudCruise normally sets up first. To keep sign-in secure, that state is required, so an unsolicited assertion can't complete a login.
To get the same one-click experience safely, point the tile at CloudCruise's **SSO initiation URL** instead of directly at the SAML app. CloudCruise starts the login itself from there, so the request state exists and the sign-in completes normally:
```
https://app.cloudcruise.com/auth/saml-init?domain=
```
Replace `` with the domain you verified in [Step 1](#step-1-verify-your-domain) (for example `acme.com`).
In your IdP, create a **bookmark** or **link** application (not a SAML application) with the URL above as its target. In Okta: **Applications → Create App Integration → Bookmark App**.
Assign the bookmark app to the same users or groups as your SAML app. Clicking it now runs the normal SSO login and lands them in CloudCruise signed in.
Not every IdP has a bookmark-app concept (for example, Auth0 doesn't). In that case, just share the SSO initiation URL above directly — as a link on your intranet, in a password manager, or as a browser bookmark. It works the same way either way.
***
## Requiring SSO (enforcement)
Each SAML configuration has a **Require SSO** toggle:
| State | Behavior |
| - | - |
| **Off** (default) | Users on the domain can sign in **either** with SSO **or** with an email/password. |
| **On** | Password sign-in and new password sign-ups are **blocked** for the domain. Anyone attempting password login is redirected to SSO. |
Enforcement is applied both in the CloudCruise sign-in flow and deep in the auth layer, so it can't be bypassed by going straight to the password form.
Turn enforcement **on only after** you've confirmed SSO login works end-to-end. CloudCruise also enforces this at the database level: **Require SSO** cannot be turned on until at least one organization **Owner** has actually signed in via SSO and linked their account, which prevents the classic case of enforcement being flipped on before anyone can use SSO yet. This doesn't protect against every failure mode — if your IdP breaks later (e.g. a certificate rotates) while enforcement is already on, you can still end up temporarily locked out. Leave it off while testing, then flip it on.
***
## User provisioning
CloudCruise uses **just-in-time provisioning**. When a user signs in through SSO for the first time, an account is created automatically from the email address in the SAML assertion — there's no separate import step. The email comes from the SAML **Name ID** (`emailAddress` format), which is why your IdP must send it there.
Just-in-time provisioning creates the CloudCruise user account and profile only. It does **not** automatically add the user to your organization or to every workspace.
To give a user access, invite them to your organization from CloudCruise. When they accept the invite, they're added as an organization **Member** and as a **Member** of the organization's existing workspaces. New workspaces can also be created with existing organization members added automatically, or workspace access can be managed separately.
If someone already has a CloudCruise account under the same email — for example, they signed up with a password before your organization set up SSO — signing in via SSO does **not** automatically merge into that existing account. SAML identities are deliberately never auto-linked to other accounts, for security reasons. Instead, CloudCruise detects the match and prompts the user to link the two accounts: if the existing account already has organization access, linking is **required** before they can continue; otherwise it's an optional prompt they can act on whenever they choose. Until they link, the two accounts stay fully separate.
For most providers the default mapping (Name ID → email) is all you need. If your IdP can only send email as a custom attribute claim, [contact support](mailto:founders@cloudcruise.com) — a custom attribute mapping can be applied to your configuration.
***
## Identity provider quick reference
The exact labels differ per provider, but the mapping is always the same.
1. **Applications → Create App Integration → SAML 2.0**.
2. **Single sign-on URL** → `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/acs`
3. **Audience URI (SP Entity ID)** → `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/metadata`
4. **Name ID format** → `EmailAddress`; **Application username** → Email.
5. Finish, then open **Sign On → View SAML setup instructions** to grab:
* **Identity Provider Issuer** → IdP Entity ID
* **Identity Provider Single Sign-On URL** → IdP SSO URL
* **X.509 Certificate** → paste as the PEM certificate
1. **Enterprise applications → New application → Create your own** (non-gallery), then **Single sign-on → SAML**.
2. **Identifier (Entity ID)** → `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/metadata`
3. **Reply URL (ACS URL)** → `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/acs`
4. Ensure the **Unique User Identifier (Name ID)** claim is the user's `user.mail` in email format.
5. From the SAML configuration panel, copy:
* **Microsoft Entra Identifier** → IdP Entity ID
* **Login URL** → IdP SSO URL
* Download the **Certificate (Base64)** → paste as the PEM certificate
1. **Admin console → Apps → Web and mobile apps → Add custom SAML app**.
2. Copy Google's **SSO URL**, **Entity ID**, and **Certificate** (these are your IdP values).
3. **ACS URL** → `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/acs`
4. **Entity ID** → `https://hrcczpkvvknatvtuwksw.supabase.co/auth/v1/sso/saml/metadata`
5. **Name ID format** → `EMAIL`; **Name ID** → Basic Information > Primary email.
***
## Managing and removing configurations
From the [**Authentication**](https://app.cloudcruise.com/organizations/authentication) page you can:
* **Toggle Require SSO** on or off at any time.
* **Delete a SAML configuration** — users on that domain can no longer sign in via SSO (delete this before removing the domain).
* **Delete a verified domain** — only allowed once its SAML configuration has been removed.
***
## Troubleshooting
Confirm the TXT record is on the exact host `_cloudcruise-sso-verification.` and the value matches `cc-sso-verify=` from the dialog. The verification dialog reports what it actually found — a stale token, a non-CloudCruise record, or nothing yet. Allow time for DNS propagation and use **Check now** to retry.
The email's domain has no SAML configuration, or you entered a domain that isn't verified. Make sure the domain is **Verified** and has a SAML configuration attached, and that the user's email domain exactly matches.
Double-check the **ACS URL** and **SP Entity ID** in your IdP exactly match the values above, and that the IdP sends the user's **email as the Name ID** in `emailAddress` format. A mismatched audience or missing email is the most common cause.
This happens when the IdP tile points directly at the SAML app (an IdP-initiated login), which CloudCruise's SAML endpoint can't complete. See [Signing in directly from your IdP](#signing-in-directly-from-your-idp-optional) — point the tile at the SSO initiation URL instead.
The **IdP SSO URL** must be HTTPS, and the **certificate** must be a PEM block beginning with `-----BEGIN CERTIFICATE-----`. Copy the full certificate including the header and footer lines.
CloudCruise prevents the most common cause of this: enforcement can't be turned on until an organization Owner has already linked their account via SSO, so there's always at least one Owner able to sign in. If you're still locked out — for example, your IdP configuration broke after enforcement was already on — [contact support](mailto:founders@cloudcruise.com) to temporarily disable enforcement so you can fix the IdP settings.
## Security notes
* CloudCruise validates each SAML assertion against the **signing certificate** you provide — rotate it in your IdP and update the configuration if it changes.
* Enforcement blocks the password path **server-side**, not just in the UI, so it cannot be bypassed.
* SSO is scoped to your **organization** for configuration and enforcement. Workspace access is still determined by CloudCruise workspace membership.
* All configuration changes (created, updated, enforcement toggled, deleted) are recorded in an audit log.
# Two-Factor Authentication
Source: https://docs.cloudcruise.com/concepts/twofactor
Seamless 2FA handling for autonomous browser agents
Browser agents frequently encounter login flows secured by two‑factor authentication (2FA). CloudCruise supports four common 2FA channels and offers multiple integration patterns so you can choose the right balance of autonomy vs. human oversight.
## Supported 2FA Channels
| Channel | Typical Form | Notes |
| - | - | - |
| **Authenticator App** | Time‑based (TOTP) 6‑digit codes generated from a shared secret. | Most secure and our preferred method when you can access the secret. |
| **SMS** | One‑time code delivered by text message. | Requires a phone number on record with the service you automate. |
| **Email** | One‑time code delivered to a mailbox. | Requires an email address on record with the service you automate. |
| **Magic Link** | Login link delivered to a mailbox. | Uses the same CloudCruise email setup as email 2FA. |
## Integration Patterns
### 1. Vault‑Stored Authenticator Secret (Recommended)
When you can export or reveal the QR‑code secret behind a TOTP authenticator, simply store that secret in the Vault entry that belongs to the credential set:
```json theme={null}
{
"username": "alice@example.com",
"password": "••••••••",
"tfa_secret": "JBSWY3DPEHPK3PXP"
}
```
You can do this with our `/vault` endpoints or using our UI [here](https://app.cloudcruise.com/vault).
At runtime the browser agent generates the current code itself, allowing fully autonomous logins with no external dependency.
> **Tip** – This approach also works for SMS or email 2FA when the underlying service lets you switch to an authenticator‑app factor.
### 2. **CC 2FA Proxy** (Hands‑Off SMS / Email / Magic Link)
CloudCruise can provide an **isolated phone number and inbox per Vault credential**. Perform a one‑time change in the target account:
1. Replace the existing 2FA phone number or email with the CloudCruise‑supplied value.
2. (Optional) Add a forwarding rule so codes are copied to a human‑accessible destination.
**Alternative**: Instead of changing the registered email, you can configure your existing inbox to forward 2FA or magic-link emails to the vault entry email address.
At run‑time the flow looks like this:
```
Service → sends code → CC Proxy → Browser Agent
```
*No webhooks or manual steps are needed; agents consume the code automatically.*
#### Pros / Cons
| Pros | Cons |
| - | - |
| Zero delay — agent stays autonomous. | One‑time account update required. |
| Dedicated channel per credential — no collision between tenants. | Forwarding rule needed if humans still log in. |
| Supports email codes and magic links. | Magic-link workflows need a TFA node configured for `MAGIC_LINK`. |
### 3. **User‑Driven Code Submission** (Minimal Setup)
If you cannot change the registered phone/email or export the authenticator secret, fall back to interactive mode:
1. Agent reaches the 2FA prompt and emits a webhook:
```json theme={null}
{
"payload": {
"current_step": "Enter 2FA Code",
"missing_properties": ["code"],
"message": "Please enter the 6-digit code sent to your phone",
"session_id": "session-123",
"workflow_id": "workflow-456",
"expected_json_schema_datamodel": {
"type": "object",
"properties": {
"code": {
"type": "string",
"pattern": "^\\d{6}$",
"description": "6-digit verification code"
}
},
"required": ["code"]
}
},
"timestamp": 1710936000,
"expires_at": 1710936300,
"event": "interaction.waiting"
}
```
2. Your application notifies the user and collects the 6‑digit code.
3. Submit the code via the **User Interaction API**:
```bash theme={null}
curl -X POST \
-H "cc-key: $CC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}' \
https://api.cloudcruise.com/run/{session_id}/user_interaction
```
#### Pros / Cons
| Pros | Cons |
| - | - |
| No changes to the service’s 2FA configuration. | Requires user attention within \~5 minutes (default expiry). |
| Works with any factor type. | Potential overnight downtime if sessions expire unattended. |
## Choosing an Approach
| Scenario | Best fit |
| - | - |
| You control the account and can export the seed. | **Vault‑Stored Secret** |
| Shared team account; you want agents + humans to log in. | **CC 2FA Proxy** with forwarding |
| The service sends email login links instead of numeric codes. | **CC 2FA Proxy** with `MAGIC_LINK` |
| Highly regulated service that forbids factor changes. | **User‑Driven Submission** |
## Error Handling & Timeouts
* A 2FA code must be delivered to the agent within **5 minutes** of the prompt.
* If the window elapses, the run fails; retry or re‑schedule as needed.
## Security Considerations
* All codes received via the CC 2FA Proxy are discarded after use.
* When you store a TOTP secret in the Vault, it is wrapped with the same envelope‑encryption scheme used for passwords.
* Webhook payloads never contain raw codes—only metadata indicating **that** a code is required.
# Vault
Source: https://docs.cloudcruise.com/concepts/vault
Where your credentials are stored
The vault is where your credentials are stored. You can add entries through our [UI](https://app.cloudcruise.com/vault) or [API](/vault-api/create-vault-entry). When adding an entry to your vault through our API, you have to encrypt the user name, password, and any `tfa_secret` before sending them to us. You can find your encryption key in your [workspace settings](https://app.cloudcruise.com/settings/encryption-keys). Here's a code snippet you can use:
```python theme={null}
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
import binascii, json
def encrypt_message(message: dict, key_hex: str) -> str:
key = binascii.unhexlify(key_hex) # 32‑byte AES‑256 key
iv = get_random_bytes(12) # 96‑bit IV for GCM
serialized = json.dumps(message) # ensure deterministic encoding
cipher = AES.new(key, AES.MODE_GCM, iv)
ciphertext, tag = cipher.encrypt_and_digest(serialized.encode())
return iv.hex() + ciphertext.hex() + tag.hex()
```
```javascript theme={null}
const crypto = require('crypto');
function encryptMessage(message, keyHex) {
// Convert hex key to bytes
const key = Buffer.from(keyHex, 'hex');
// Generate a random initialization vector (IV)
const iv = crypto.randomBytes(12);
// Serialize message
const serializedMessage = JSON.stringify(message);
// Create cipher object and encrypt the message
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(serializedMessage, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Get authentication tag
const tag = cipher.getAuthTag().toString('hex');
// Return IV, encrypted message, and tag as hex strings
return iv.toString('hex') + encrypted + tag;
}
```
# Security Overview
Our credential vault adopts the envelope‑encryption pattern recommended by NIST SP 800‑57 and mirrors proven designs used by services like AWS Secrets Manager and Google Secret Manager. **Plaintext credentials are never persisted or transmitted to our servers**. This page outlines the key design choices behind that guarantee.
## Key hierarchy
* **Per‑user data key (AES‑256‑GCM)**
* Can be generated and managed in your [workspace settings](https://app.cloudcruise.com/settings/encryption-keys)
* Stored **only in encrypted form**.
* Can be **rotated at any time** from the dashboard.
* **Vault master key** – a 256‑bit key held in AWS KMS/HSM.
* Used solely to encrypt/decrypt each user’s data key.
* Stored as a Kubernetes Secret
## Secret ingestion flow
1. Client encrypts the credential locally with their data key.
2. The ciphertext is sent to the vault. The vault verifies the envelope and stores it verbatim.
3. The vault keeps a mapping: `user‑id → { encrypted_data_key, ciphertext, metadata }`.
> The vault **rejects** any credential that is not already encrypted with the correct data key for that user.
## Runtime access
When a browser agent needs a credential:
1. The agent requests the ciphertext.
2. The vault decrypts the data key entirely in‑memory using the master key mounted from the Kubernetes Secret.
3. The agent uses it, and immediately zeroises the buffer.
4. No decrypted value is logged, cached, or exported.
## Key rotation
Users may rotate their data key at any time. The vault automatically:
1. Generates a new data key.
2. Decrypts each stored secret with the old key within a secure enclave.
3. Re‑encrypts the secret with the new key and updates metadata.
4. Shreds the old key material.
## Threat model & mitigations
| Threat | Mitigation |
| - | - |
| Database breach | Secrets remain encrypted with per‑user keys; attacker lacks the data keys. |
| Compromise of a single data key | Blast radius limited to that user only. |
| Master key exposure | Master key stored in KMS with hardware isolation & strict IAM; usage logged and alerted. |
| Replay/tampering | AES‑GCM tag validation prevents bit‑flips or stale ciphertext from being accepted. |
For security questions or key‑rotation assistance, email **[founders@cloudcruise.com](mailto:founders@cloudcruise.com)**.
# Credential Configuration
Vault entries support comprehensive authentication and session management options to accommodate different website requirements and security needs.
## Two-Factor Authentication (TFA)
CloudCruise supports multiple TFA methods for automated login flows:
### Authenticator App (TOTP)
* **Method**: `AUTHENTICATOR`
* **Setup**: Provide the TOTP secret key from your authenticator app
* **Usage**: CloudCruise generates time-based one-time passwords automatically during login
### Email TFA
* **Method**: `EMAIL`
* **Setup Options**:
1. Register the CloudCruise email directly with the target account
2. Forward TFA emails from your account to the CloudCruise email
* **Usage**: CloudCruise automatically processes TFA codes received via email
### Email Magic Links
* **Method**: `MAGIC_LINK`
* **Setup Options**:
1. Register the CloudCruise email directly with the target account
2. Forward magic-link emails from your account to the CloudCruise email
* **Usage**: CloudCruise extracts the login link from the email and navigates to it during the TFA node
### SMS TFA
* **Method**: `SMS`
* **Setup Options**:
1. Register the CloudCruise phone number directly with the target account
2. Forward TFA SMS messages to the CloudCruise phone number
* **Usage**: CloudCruise automatically processes TFA codes received via SMS
## Session Persistence
Control how browser state is maintained across workflow executions:
### Storage Options
* **Cookies**: Maintains authentication cookies between runs
* **Local Storage**: Preserves local storage data across sessions
* **Session Storage**: Maintains session storage (typically cleared between browser sessions)
### Concurrent Sessions
* **Allow Multiple Sessions**: Enable parallel workflow executions using the same credentials
* **Max Concurrent Sessions**: Limit the number of simultaneous sessions (leave empty for no limit)
* **Prevent Concurrency During Login**: Block other sessions while login is in progress
### Session Expiration
Configure automatic session cleanup:
* **Expiry from Last Use**: Sessions expire after period of inactivity
* **Expiry from Creation**: Sessions expire after absolute time from creation
## Credential Pooling
Distribute runs across multiple accounts by passing an array of `permissioned_user_id` values instead of a single string when starting a run:
```json theme={null}
{
"run_input_variables": {
"USER": [
"user-id-1",
"user-id-2",
"user-id-3"
]
}
}
```
```typescript theme={null}
const run = await client.runs.start({
workflow_id: "your-workflow-id",
run_input_variables: {
USER: ["user-id-1", "user-id-2", "user-id-3"],
},
});
```
```python theme={null}
run = client.runs.start(
StartRunRequest(
workflow_id="your-workflow-id",
run_input_variables={
"USER": ["user-id-1", "user-id-2", "user-id-3"],
},
)
)
```
CloudCruise uses round-robin selection to distribute runs across the provided credentials. Each credential's concurrency limits and session settings are respected individually. This helps prevent rate limiting on target websites and provides fault tolerance if one credential becomes unavailable.
## Proxy Configuration
Override workflow proxy settings at the credential level:
### Proxy Modes
Set a credential's proxy via `proxy_setting` (it overrides the workflow-level proxy for any run that uses this credential). The meaning of `proxy_value` depends on the mode:
| `proxy_setting` | `proxy_value` | Behavior |
| - | - | - |
| `random` | — | A random proxy from CloudCruise's managed pool |
| `static` | target IP address | The managed proxy closest to that IP — ideal for geo-restricted content or region-specific testing |
| `country` | country code (e.g. `US`) | A managed proxy in that country |
| `custom` | proxy URL | Your own proxy infrastructure (Enterprise) — see [Custom URL](#custom-url-bring-your-own-proxy) below |
**Proxy priority:** credential-level proxy settings take precedence over workflow-level configuration, allowing fine-grained control over network routing for specific accounts.
### Custom URL (bring-your-own proxy)
Enterprise customers can point CloudCruise at their own proxy infrastructure instead of the managed pool. Select **Custom URL** in the credential's proxy dropdown and provide a URL.
**Supported transports and authentication:**
| Form | Auth | Use case |
| - | - | - |
| `socks5://user:pass@host:port` | SOCKS5 username/password | Most residential and self-hosted SOCKS proxies |
| `socks5://host:port` | None (IP allowlist) | SOCKS proxies that whitelist worker egress IPs |
| `http://user:pass@host:port` | HTTP CONNECT + Basic auth | Squid, Tinyproxy, most corporate egress gateways |
| `http://host:port` | None (IP allowlist) | Corporate gateways that whitelist worker egress IPs |
| `https://user:pass@host:port` | HTTP CONNECT inside TLS + Basic auth | Proxies that require TLS encryption to the proxy itself |
| `https://host:port` | None (IP allowlist) | TLS-to-proxy with IP allowlist |
**Not supported (today):** NTLM / Negotiate / Kerberos auth, HTTP Digest auth, and bearer-token / custom-header authentication. If your proxy requires one of these, contact us — we can scope a follow-up.
**UDP behavior:**
**Preferred:** a SOCKS5 proxy that supports UDP (UDP ASSOCIATE). This preserves full UDP egress — QUIC, WebRTC, and UDP DNS. If your proxy is HTTP CONNECT or a TCP-only SOCKS deployment, custom proxying still works, but the worker automatically falls back to DNS-over-TCP: HTTP/3 (QUIC) degrades to HTTP/2 over TCP (pages still load) and WebRTC-over-UDP is unavailable.
* HTTP CONNECT proxies cannot carry UDP by spec. CloudCruise routes DNS over TCP through the proxy so resolution still works; Chrome's QUIC attempts fail fast and fall back to TCP/HTTP2 automatically; WebRTC over UDP is unavailable.
* SOCKS proxies vary — the spec supports UDP via UDP ASSOCIATE but many real-world deployments are TCP-only. On the first activation of a custom SOCKS proxy CloudCruise probes UDP support via a short DNS query through the proxy. If UDP works, full UDP egress (QUIC, WebRTC, UDP DNS) is preserved. If UDP doesn't work, the worker reactivates with TCP DNS — same fallback as HTTP CONNECT. The probe result is cached for 24 h.
**What CloudCruise does with the URL:**
* The URL is encrypted at rest with the workspace's AES-256-GCM key (same protection as vault passwords).
* On every dispatch, the URL is re-validated against SSRF rules: private/loopback/link-local IPs (incl. cloud metadata services at 169.254.169.254), `.local`/`.internal`/etc. suffixes, and hosts that resolve to CloudCruise infrastructure are all rejected.
* For `http`/`socks5` schemes, the hostname is DNS-pinned at dispatch to eliminate rebinding windows. For `https`, the hostname is preserved for TLS SNI validation.
**Trust model:**
* Target-site traffic flows through your proxy. The proxy operator can see and log everything the worker sends to and receives from the target site for plain HTTP. For HTTPS, traffic is end-to-end encrypted to the target — your proxy sees the SNI and connect-traffic envelope but not the request/response body unless it performs TLS interception with a CA that the worker trusts (which is not possible on CloudCruise-managed workers).
* CloudCruise infrastructure traffic (auth tokens, websocket, telemetry, screenshot uploads) is **bypassed** — it never traverses your proxy. The bypass covers `*.cloudcruise.com`, `*.cloudcruise.app`, and your configured Supabase host.
* If sing-box (the OS-level proxy router) dies mid-session, the worker fails the run rather than reverting to direct internet egress. This protects compliance use cases where all automation traffic must stay inside the customer's network.
**Compliance note:** when a custom proxy is active, traffic to the target site is egressing through customer-controlled infrastructure. The customer is the data controller for that egress, including any credentials submitted to the target site through their proxy.
# Webhooks
Source: https://docs.cloudcruise.com/concepts/webhooks
Register webhooks to receive updates on the workflow execution
You can register webhooks to receive execution events. Webhooks can be scoped to a single workflow or registered globally for all workflows in your workspace.
**Per-workflow webhooks** fire only for runs of a specific workflow.
**Global webhooks** fire for every workflow in your workspace. Every event payload includes a `workflow_id` field so you can route on your end. Register them in [Settings → Webhooks](https://app.cloudcruise.com/settings/webhooks) by selecting "All Workflows" as the scope.
For the full list of subscribable events you can set in `event_types_subscribed`, see the webhook schema in the [Run API reference](/run-api/start-a-run). Maintenance recovery flows emit the lifecycle events documented below, such as `execution.requeued`, `execution.input_required`, `execution.password_updated`, and the final terminal status.
This event is sent once when the execution of the workflow starts. It returns a live view URL of the running browser agent embeddable as iframe.
```json theme={null}
{
"event": "execution.start",
"payload": {
"session_id": "64355337-7561-45ec-82bc-b6266e11208c",
"workflow_id": "745fdceb-53bf-4562-95dc-10495e303f0c",
"live_view_url": "https://cyclone.cloudcruise.com/..."
},
"metadata": {
"key1": "value1"
},
"timestamp": 1750068153,
"expires_at": 1750068453
}
```
This event is sent every time the workflow is executing an action like clicking on a button or filling out an input field.
```json theme={null}
{
"payload": {
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731",
"current_step": "Type user name",
"current_step_type": "INPUT_TEXT",
"next_step": "Click Next",
"next_step_type": "CLICK"
},
"metadata": {
"key1": "value1"
},
"timestamp": 1724002463,
"expires_at": 1724002763,
"event": "execution.step"
}
```
This event is sent when a run is queued and waiting for an available worker.
```json theme={null}
{
"event": "execution.queued",
"payload": {
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731"
},
"metadata": {
"key1": "value1"
},
"timestamp": 1724002463,
"expires_at": 1724002763
}
```
This event is sent when a run is paused before execution (for example, when workflow-level or credential-level pause controls are active).
```json theme={null}
{
"event": "execution.paused",
"payload": {
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731",
"pause_reasons": [
{
"resource_type": "workflow",
"resource_id": "00b82d36-8243-4a95-a0ca-e64096226731",
"reason": "Paused by workspace admin"
}
]
},
"timestamp": 1724002463,
"expires_at": 1724002763
}
```
This event is sent once when the execution of the workflow has finished successfully. It includes the result data that is returned from the workflow.
```json theme={null}
{
"payload": {
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731",
"data": {
"your": "defined",
"output": "datamodel",
},
"errors": [],
"status": "execution.success",
"file_urls": [],
"input_variables": {
"USER_NAME": "adrian"
},
"encrypted_variables": [
"USER_NAME"
],
"vault_entries": {
"my_credential": {
"USER_ID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"USER_ALIAS": "My Login",
"USER_NAME": "encrypted_value",
"PASSWORD": "encrypted_value",
"DOMAIN": "https://example.com"
}
},
"screenshot_urls": [
{
"signed_screenshot_url": "https://cloudcruise.com/screenshots/...",
"signed_screenshot_url_expires": "2025-06-01T22:21:00.371579+00:00",
"error_screenshot": false,
"node_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"node_display_name": "Click on login",
"timestamp": "2025-05-25T22:21:00.371579+00:00"
}
],
"video_urls": [
{
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"signed_screen_recording_url": "https://cloudcruise.com/videos/...",
"signed_screen_recording_url_expires": "2025-06-01T22:21:00.371579+00:00",
"timestamp": "2025-05-25T22:21:00.371579+00:00"
}
]
},
"metadata": {
"key1": "value1"
},
"timestamp": 1724002463,
"expires_at": 1724002763,
"event": "execution.success"
}
```
This event is sent once the execution of the workflow fails and contains information about the error.
```json theme={null}
{
"payload": {
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731",
"data": {
"your": "defined",
"output": "datamodel",
},
"errors": [{
"message": "The login failed. The website showed an error that the credentials are invalid.",
"error_id": "c4c7d4fe-b00c-49d8-ab67-40808acea59c",
"error_code": "E001: LOGIN_FAILED",
"llm_error_category": "AUTHENTICATION_ERROR",
"llm_error_sub_type": "INVALID_CREDENTIALS",
"original_error": "Element not found: //button[@id='login-submit']",
"full_url": "https://cloudcruise.com/login",
"action_type": "CLICK",
"action_display_name": "Click on login"
}],
"status": "execution.failed",
"file_urls": [],
"input_variables": {
"$USER_NAME": "adrian"
},
"encrypted_variables": [
"USER_NAME"
],
"vault_entries": {
"my_credential": {
"USER_ID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"USER_ALIAS": "My Login",
"USER_NAME": "encrypted_value",
"PASSWORD": "encrypted_value",
"DOMAIN": "https://example.com"
}
},
"screenshot_urls": [
{
"signed_screenshot_url": "https://cloudcruise.com/screenshots/...",
"signed_screenshot_url_expires": "2025-06-01T22:21:00.371579+00:00",
"error_screenshot": true,
"node_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"node_display_name": "Click on login",
"timestamp": "2025-05-25T22:21:00.371579+00:00"
}
],
"video_urls": [
{
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"signed_screen_recording_url": "https://cloudcruise.com/videos/...",
"signed_screen_recording_url_expires": "2025-06-01T22:21:00.371579+00:00",
"timestamp": "2025-05-25T22:21:00.371579+00:00"
}
]
},
"timestamp": 1724002463,
"expires_at": 1724002763,
"event": "execution.failed"
}
```
The `llm_error_category` field contains an AI-generated categorization of the error type. This field may be `null` if AI analysis was not performed.
The `llm_error_sub_type` field contains a more specific sub‑classification within the category. See the [error classification matrix](/concepts/maintenance-agent#error-classification-matrix) for all possible values. This field may be `null` if no sub‑type applies.
The `original_error` field contains the raw technical runtime error message before any AI analysis. This is useful for debugging and differs from `message` field that contains the Maintenance Agent's analysis.
When a workflow has `deny_requeue_past_dry_run_marker` enabled and a failure occurs at or past the dry-run marker, the run is not retried. If no customer-configured error code matches the failure, the terminal error code is set to `requeue_denied_past_dry_run_marker`; otherwise the matched customer code takes precedence. See [Service and Logged-Out Session Recovery](/concepts/maintenance-agent#service-and-logged-out-session-recovery) for details.
This event is sent when one or several workflow runs were cancelled. It respectively either contains `session_id` of the cancelled run or `session_ids` that contains all of the cancelled runs.
Response for single run cancelled:
```json theme={null}
{
"payload": {
"message": "User interrupted workflow. Session was stopped.",
"error_code": "INTERRUPTED-E0001",
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
},
"timestamp": 1724002463,
"expires_at": 1724002763,
"event": "execution.stopped"
}
```
Response for multiple cancelled at once:
```json theme={null}
{
"payload": {
"message": "Execution was interrupted because another workflow was executed with the same credentials, but failed its login attempt. This is to prevent repeated incorrect login attempts.",
"error_code": "INTERRUPTED-E0002",
"session_causing_error": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"session_ids": ["60673dc6-92a1-428a-ba51-8c3f324daea8", "50673dc6-92a1-428a-ba51-8c3f324daea8"]
},
"metadata": {
"key1": "value1"
},
"timestamp": 1724002463,
"expires_at": 1724002763,
"event": "execution.stopped"
}
```
This event is sent when a failed run is automatically requeued for retry by the Maintenance Agent (e.g., after a `SERVICE_UNAVAILABLE` classification). See [Service Unavailable Recovery](/concepts/maintenance-agent#service-unavailable-recovery) for retry schedule details.
```json theme={null}
{
"event": "execution.requeued",
"payload": {
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731",
"retry_attempt": 2,
"max_retries": 10,
"next_execution_time": "2026-05-26T23:30:00.000Z",
"delay_ms": 1200000
},
"metadata": {
"key1": "value1"
},
"timestamp": 1750068153,
"expires_at": 1750068453
}
```
This event is sent when a run pauses because incorrect input data was detected and the workflow is waiting for corrected values. Submit corrected fields using the [Submit New Input Variables endpoint](/run-api/submit-new-input-variables) to resume execution.
```json theme={null}
{
"event": "execution.input_required",
"payload": {
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"input_variables": {
"patient_id": "ABC-123"
},
"screenshot_url": "https://cloudcruise.com/screenshots/..."
},
"timestamp": 1724002463,
"expires_at": 1724002763
}
```
This event is sent after an automated password recovery flow succeeds and CloudCruise updates the vault credential used by the run.
```json theme={null}
{
"event": "execution.password_updated",
"payload": {
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"workflow_id": "00b82d36-8243-4a95-a0ca-e64096226731"
},
"timestamp": 1724002463,
"expires_at": 1724002763
}
```
This event is sent when the upload of the screenshot has finished.
| Field | Description |
| - | - |
| `error_screenshot` | `true` when the screenshot was captured during an error. |
| `full_length_screenshot` | `true` when the screenshot is a full-page capture (scrolled and stitched). This happens for dedicated Screenshot nodes and error screenshots. Viewport-only captures set this to `false`. |
| `retry_index` | The retry attempt index for the run (0 = first attempt). |
```json theme={null}
{
"payload": {
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"screenshot_id": "6fdfad4a-3064-48cf-99f0-3d8d271f2c8e",
"signed_screenshot_url": "https://cloudcruise.com/screenshots/...",
"signed_screenshot_url_expires": "2024-08-25T17:35:00.432Z",
"node_display_name": "Click on login",
"node_id": "a1b2c3d4-5678-9abc-def0-1234567890ab",
"error_screenshot": true,
"timestamp": "2024-08-30T17:35:00.432Z",
"retry_index": 0,
"full_length_screenshot": false,
"metadata": {}
},
"metadata": {
"key1": "value1"
},
"timestamp": 1724002463,
"expires_at": 1724002763,
"event": "screenshot.uploaded"
}
```
This event is sent when the upload of the workflow video has finished.
```json theme={null}
{
"payload": {
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
"signed_screen_recording_url": "https://cloudcruise.com/...",
"signed_screen_recording_url_expires": "2025-06-01T22:21:00.371579+00:00",
"timestamp": "2025-05-25T22:21:00.371579+00:00"
},
"metadata": {
"key1": "value1"
},
"timestamp": 1724002463,
"expires_at": 1724002763,
"event": "video.uploaded"
}
```
This event is sent when a file has been uploaded during workflow execution. The `signed_file_url` expiration is configurable per workspace (default: 7 days, range: 1 hour to 7 days). Configure this in your [workspace settings](https://app.cloudcruise.com/settings/data-retention).
```json theme={null}
{
"payload": {
"signed_file_url": "https://example.com/storage/v1/object/sign/files/123/456/example.zip?token=dummy-token",
"file_name": "example_file.zip",
"timestamp": "2024-03-20T12:00:00.000Z",
"signed_file_url_expires": "2024-03-27T12:00:00.000Z",
"metadata": {
"key1": "value1"
},
"session_id": "60673dc6-92a1-428a-ba51-8c3f324daea8",
},
"timestamp": 1710936000,
"expires_at": 1710936300,
"event": "file.uploaded"
}
```
This event is sent when user input is required to continue the workflow.
```json theme={null}
{
"payload": {
"current_step": "Enter 2FA Code",
"missing_properties": ["two_factor_code"],
"message": "Please enter the 6-digit code sent to your phone",
"session_id": "session-123",
"workflow_id": "workflow-456",
"expected_json_schema_datamodel": {
"type": "object",
"properties": {
"two_factor_code": {
"type": "string",
"pattern": "^\\d{6}$",
"description": "6-digit verification code"
}
},
"required": ["two_factor_code"]
}
},
"timestamp": 1710936000,
"expires_at": 1710936300,
"event": "interaction.waiting"
}
```
This event is sent when the user input was not provided in time or in a wrong format.
```json theme={null}
{
"timestamp": 1724002463,
"expires_at": 1724002763,
"event": "interaction.failed"
}
```
This event is sent when the user input was provided and the workflow continues.
```json theme={null}
{
"payload": {
"current_step": "Enter 2FA Code",
"message": "Please enter the 6-digit code sent to your phone",
"session_id": "session-123",
"workflow_id": "workflow-456",
"provided_input": {
"two_factor_code": 123456
},
"expected_json_schema_datamodel": {
"type": "object",
"properties": {
"two_factor_code": {
"type": "string",
"pattern": "^\\d{6}$",
"description": "6-digit verification code"
}
},
"required": ["two_factor_code"]
}
},
"timestamp": 1724002463,
"expires_at": 1724002763,
"event": "interaction.finished"
}
```
Simply navigate to our [settings](https://app.cloudcruise.com/settings) page to get started.
## Security
We sign webhook events with the secret you get when you register a webhook. We also add an expiry time. You can change the expiry time in the setting of the registered webhook.
Here's how we recommend to verify the message you receive from us:
```python theme={null}
import time
import hashlib
import hmac
import json
class VerificationError(Exception):
"""Custom exception to handle verification errors."""
def __init__(self, message="Verification failed", status_code=400):
super().__init__(message)
self.status_code = status_code
def verify_hmac(received_data, received_signature, secret_key):
# Ensure the received data is in bytes, necessary for HMAC
if isinstance(received_data, str):
received_data = received_data.encode()
# Generate the HMAC new object with the secret key and SHA-256
calculated_signature = hmac.new(secret_key.encode(), received_data, hashlib.sha256).hexdigest()
# Safely compare the computed HMAC with the received HMAC
return hmac.compare_digest(calculated_signature, received_signature)
# Extract signature from X-HMAC-Signature header, removing the 'Sha256=' prefix
# receivedData = await request.body()
# receivedSignature = request.headers.get('X-HMAC-Signature').split('=')[1]
# secretKey = your_secret_key
def verify_message(received_data, received_signature, secret_key):
if not received_data:
raise VerificationError("Received request without body", 400)
try:
data_json = json.loads(received_data.decode('utf-8'))
except json.JSONDecodeError as e:
raise VerificationError("Failed to decode json: " + str(e), 400)
# Check if the expiration is sent
expires_at = data_json.get("expires_at")
if not expires_at:
raise VerificationError("No expiration date sent", 400)
# Verify HMAC first
if not verify_hmac(received_data, received_signature, secret_key):
raise VerificationError("Invalid HMAC signature.", 401)
# Check if the message is not expired
if time.time() > expires_at:
raise VerificationError("Webhook message expired.", 400)
return data_json
```
Install the package by following the [TS/JS SDK overview](/sdk/js/overview).
```javascript theme={null}
import express from "express";
import { CloudCruise, VerificationError, WebhookEventType } from "cloudcruise";
const client = new CloudCruise({
apiKey: process.env.CLOUDCRUISE_API_KEY,
encryptionKey: process.env.CLOUDCRUISE_ENCRYPTION_KEY,
});
const app = express();
// Use a raw body parser so the SDK can verify the signature accurately
app.post(
"/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
try {
const signature = req.get("x-hmac-signature");
const event = client.webhook.verifySignature(
req.body,
signature,
process.env.CLOUDCRUISE_WEBHOOK_SECRET
);
switch (event.event) {
case WebhookEventType.ExecutionSuccess:
console.log("Workflow completed successfully");
break;
case WebhookEventType.ExecutionFailed:
console.log("Workflow failed");
break;
}
res.status(200).json({ status: "received" });
} catch (error) {
console.error("Invalid webhook", error);
res.status(400).json({ error: "Invalid webhook" });
}
}
);
```
## IP Allowlisting
All webhook requests from CloudCruise originate from the following static IP addresses. If your infrastructure requires IP-based filtering, add these to your allowlist:
```
35.164.71.220
54.191.200.70
18.236.26.110
```
## Payload Size
A registration can be configured to omit `input_variables` from the payloads it receives. Turn on **Exclude input variables from the payload** in the webhook dialog in [Settings → Webhooks](https://app.cloudcruise.com/settings/webhooks), or send `excludeInputVariables: true` when creating or updating the webhook through the API.
The field is then left out of every event delivered to that endpoint. Use it when you start runs with large inputs and your endpoint enforces a body-size limit — you already hold the inputs you sent. Other registrations are unaffected, and the run result from `GET /runs/{session_id}` still returns the inputs.
`excludeInputVariables` is optional on update: leave it out and the endpoint keeps whatever setting it already has.
The setting applies from the moment you turn it on. Events recorded before then were stored with `input_variables` included, and [replaying](#manual-replay) one re-sends it as recorded — replay never rebuilds the payload. If you enable the option to get under a body-size limit, expect replays of earlier events to still exceed it.
## Deliverability
CloudCruise automatically retries failed webhook deliveries up to **3 times** (4 total attempts including the initial request) with exponential backoff delays between attempts.
Each delivery attempt has a 5-second request timeout. Total delivery time depends on failure mode, so retries may complete quickly for immediate network errors or take longer for repeated timeouts.
### Retry Conditions
A webhook delivery is retried when:
* **Network errors** occur (connection timeout, DNS failure, etc.)
* **Server errors** are returned (HTTP status codes 500 and above)
* **Request timeouts** exceed 5 seconds
Webhooks are **not** retried for client errors (4xx status codes) as these typically indicate configuration issues that won't resolve with retries.
### Manual Replay
If webhooks still fail after automatic retries, you can manually replay them:
* **Dashboard**: Navigate to the run detail view and trigger a resend
* **API**: Send a POST request to `https://api.cloudcruise.com/webhooks/{session_id}/replay`
The replay endpoint will re-send all webhook events that were recorded for the given session.
# Workflow Components
Source: https://docs.cloudcruise.com/concepts/workflow-components
Reusable node groups that stay in sync across workflows
Workflow components let you save a group of nodes as a reusable building block. When you paste a component into other workflows, it stays linked — updating the component propagates changes to every workflow that uses it.
## Core concepts
| Term | Meaning |
| - | - |
| **Component** | A saved group of nodes with their internal edges, stored in your workspace's component library. |
| **Instance** | A copy of a component inside a specific workflow. Instances stay linked to the library component they came from. |
| **Propagation** | When you update a component, every workflow in the workspace that contains an instance of it is automatically updated to match. |
## Saving a component
1. Select two or more nodes in the workflow visualizer.
2. Right-click and choose **Save as Component**.
3. Give it a name and confirm.
The selected nodes are saved to your workspace's component library and tagged with a blue overlay border in the current workflow. This overlay indicates they're linked to a library component.
Avoid including **Start** or **End** nodes in a component. Pasting them into another workflow would create duplicate start/end points.
## Using a component
Right-click the canvas and open the **Components** section in the add-node menu. Selecting a component pastes its nodes into the workflow at the cursor position. The pasted nodes are linked instances — they show the blue overlay border and will receive future updates from the library.
A workflow can contain **multiple instances of the same component**. Each paste creates a fresh `source_component_instance_id`, and propagation tracks each instance independently — updating the component updates every instance in every workflow.
### Vault credentials
If the source component references vault credentials (e.g., a TFA node configured with a vault alias), the relevant vault schema entries are automatically merged into the target workflow when you paste. Your existing entries are never overwritten — target entries take precedence if there's a conflict.
## Editing and updating
### Detecting changes
When you modify a component instance's nodes in a workflow (change parameters, rename, add or remove nodes), CloudCruise tracks the diff against the library version. A dirty indicator appears on the component overlay.
When you save the workflow, a dialog shows what changed and lets you choose whether to push the update back to the library component.
### Propagation
When you update a component (either from the save dialog or the context menu), all workflows in your workspace that contain an instance of that component are updated automatically:
* **Node matching** — Existing instance nodes are matched to the updated template by their stable mapping ID. New template nodes get fresh workflow node IDs.
* **Edge preservation** — Internal edges (between component nodes) are replaced with the new template. External edges (connections from component nodes to non-component nodes) are preserved.
* **Vault credentials** — Only vault entries actually referenced by the component's node parameters are merged into each target workflow. Target entries always take precedence.
You can disable propagation per-update by unchecking the propagate checkbox in the update dialog.
### Version history
Every update creates a new version. You can view the full version history from the component's context menu and revert to any previous version. Reverting creates a new version and propagates the reverted state to all instances.
## Unlinking and removing
| Action | What it does |
| - | - |
| **Split from Component** | Unlinks a single node from the component. The node stays in the workflow but is no longer part of the component instance. |
| **Clear Component** | Unlinks all nodes in the component instance. The nodes remain but lose their component association. |
| **Remove Component** | Deletes all nodes belonging to that component instance from the current workflow. Does not delete the library component. |
| **Delete Component** | Deletes the component from the library entirely. Nodes in workflows that used it become plain unlinked nodes. |
## Managing components from the CLI
The same lifecycle is scriptable via the [CloudCruise CLI](/sdk/cli/overview#workflow-components) — useful for coding agents (Claude, Cursor), CI flows, and bulk operations:
```bash theme={null}
cloudcruise components list # see what's in your workspace
cloudcruise components usage # check affected workflows before updating
cloudcruise components update --file new.json --version-note "…" # update + propagate (default)
cloudcruise components update --file new.json --no-propagate # update without propagating
cloudcruise components delete # permanent delete (irreversible)
```
See the [CLI overview](/sdk/cli/overview#workflow-components) for the full command set or the [REST endpoints](/workflow-api/list-components) for direct API integration.
## Scope and limitations
* Components are **workspace-scoped**. They can be used in any workflow within the same workspace but are not shared across workspaces.
* **Many instances per workflow are supported.** A component can be pasted into a workflow any number of times — each instance is tracked independently and updated by propagation.
* **Input/output schemas are per-workflow**, not per-component. A component does not carry or propagate input/output schema — each workflow maintains its own.
* Component updates propagate only to workflows in the **same workspace**. If a teammate doesn't have read access to a particular workflow (via RLS), that workflow won't be updated.
* **Component deletion is irreversible** — version history is wiped along with the component. Embedded instances in workflows remain as orphaned nodes (their structure persists but they no longer receive propagation).
# API Flow
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/api-flow
Replay a chain of HTTP requests as one node
The **API Flow** node sends a chain of HTTP (XHR/fetch) requests as one atomic step. Use it instead of UI interactions when a site's internal API returns the data you need, or performs the action you want, directly. It is faster and less brittle than clicking through the UI.
By default the requests run in the browser's live session, so the site's cookies attach natively. You never read, store or pass cookies yourself. With `credentials: "omit"`, the requests run cookie-free through a server-side relay instead, for endpoints that don't need the user's session.
## Example
Search for a story, then fetch the top result. The second step reads a value extracted by the first:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Fetch top story",
"action": "API_FLOW",
"parameters": {
"target_step_id": "story",
"steps": [
{
"kind": "http",
"id": "search",
"label": "Search stories",
"method": "POST",
"url": "https://api.example.com/search",
"body": {
"type": "json",
"value": { "query": "{{ context.inputs.search_query }}", "page": 0 }
},
"replay_target": {},
"extract": [
{ "name": "top_id", "source": "response_body", "expression": "hits[0].id", "required": true }
]
},
{
"kind": "http",
"id": "story",
"label": "Fetch top story",
"method": "GET",
"url": "https://api.example.com/items/{{ steps.search.top_id }}",
"replay_target": {},
"extract": [
{ "name": "title", "source": "response_body", "expression": "title", "required": true }
]
}
],
"field_json_map": "{ \"top_story_title\": steps.story.title }"
}
}
```
After the node runs, `{{context.top_story_title}}` holds the title and it appears in the run result.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `steps` | array | Yes | Requests to send. See [Steps](#steps) |
| `target_step_id` | string | Yes | ID of the step whose result the node publishes |
| `field_json_map` | string | No | JSONata expression evaluated over `{ steps, context }`. Its top-level keys merge into the workflow context, so `{ "total": ... }` lands at `context.total` and appears in the run result. Nest keys under `runtime` to keep a value available to later nodes but out of the result |
| `credentials` | string | No | `include` (default), `same-origin`, or `omit`. `omit` runs the requests cookie-free through a server-side relay |
| `redirect_policy` | string | No | `follow` (default) or `manual`. `manual` captures a redirect's `Location` instead of following it, for example to read an OAuth `code`. A step can override it with its own `redirect_policy` |
| `ordering` | array | No | `{ from_step_id, to_step_id, reason }` entries that document a dependency with no value flowing between the steps, such as a request that only sets a cookie. `reason` is `session_state`, `side_effect`, or `explicit_order`. The `steps` array must already list the steps in that order |
| `run_if` | object | No | Run the node only if the condition matches; otherwise skip it and continue on the `to` edge |
## Steps
| Field | Type | Required | Description |
| - | - | - | - |
| `id` | string | Yes | Unique within the node. Lowercase letter first, then lowercase letters, digits or `_`, up to 64 characters |
| `label` | string | Yes | Human-readable name |
| `method` | string | Yes | `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, or `OPTIONS` |
| `url` | string | Yes | Full URL. May contain `{{ }}` references |
| `replay_target` | object | Yes | Where the request is sent from. Use `{}` (top frame) unless the request must come from inside an iframe |
| `kind` | string | No | `http` |
| `query` | object | No | Query parameters. An array value repeats the parameter |
| `headers` | object | No | Request headers. Don't set `Cookie` or `Authorization` by hand; they come from the session or from an extracted value |
| `body` | object | No | Request body. See [Body](#body). Omit for no body |
| `extract` | array | No | Values to read from the response. See [Extract](#extract) |
| `repeat` | object | No | Turns the step into pagination. See [Pagination](#pagination) |
| `expected_status` | object | No | `{ "range": "2xx" }` (default) or `{ "values": [200, 201] }` |
| `retry` | object | No | `{ max_attempts, retry_on_status?, retry_on_network_error? }`, up to 5 attempts. Not allowed on `POST` or `PATCH` steps |
| `timeout_ms` | number | No | Request timeout |
| `on_error` | object | No | `{ "message": "..." }` replaces the default failure message when the step fails |
| `save_response_as_file` | object | No | `{ "file_name": "report.pdf" }` stores the response as a file. Target step only, PDF responses only, and not combined with `extract`, `repeat`, `credentials: "omit"` or `redirect_policy: "manual"` |
| `protocol_hint` | string | No | `rest`, `graphql`, or `other`. Informational only |
| `graphql_error_policy` | string | No | `fail_on_any`: fail the step if a GraphQL response contains errors |
### Value Flow
Steps run in array order. Later steps read earlier results with inline references: `{{ steps.. }}` in `url`, `query`, `headers`, or a `body` value. List every step after the steps it references. Workflow variables such as `{{ context.inputs.order_id }}` work in the same fields.
### Extract
Each entry has a `name` and one of three sources:
| Source | Shape | Reads |
| - | - | - |
| `response_body` | `{ name, source: "response_body", expression }` | JSONata path into the parsed body, e.g. `hits[0].id` |
| `response_header` | `{ name, source: "response_header", header }` | A response header |
| `response_text` | `{ name, source: "response_text", pattern, flags?, group? }` | Regex match over the raw response text. `group` picks a capture group |
Set `required: true` to fail the step when no value is found. Without it, a missing value silently becomes empty in later references. A `response_text` extraction returns one match; to collect many, use `$match` in `field_json_map`.
### Body
| `type` | Shape |
| - | - |
| `json` | `{ "type": "json", "value": }`. Put `{{ }}` references directly in as values |
| `form_urlencoded` | `{ "type": "form_urlencoded", "value": { "field": "value" } }` |
| `text` | `{ "type": "text", "value": "raw string" }` |
| `multipart` | `{ "type": "multipart", "parts": [...] }`. Each part is `{ name, kind: "text", value }` or `{ name, kind: "file", filename?, content_type?, source }` |
| `binary` | `{ "type": "binary", "source": ..., "content_type"?: ... }` |
A file `source` is `{ "type": "signed_url", "url" }`, `{ "type": "workflow_input", "path" }`, or `{ "type": "prior_step", "step_id", "extraction_name" }`.
### Pagination
`repeat` sends the step repeatedly and collects the pages:
| Field | Description |
| - | - |
| `cursor.from` | Reads the next cursor from each response. Same shapes as an `extract` entry, without `required` |
| `cursor.into` | `{ location: "url" \| "query" \| "header" \| "body", path }`: where the cursor goes in the next request |
| `until` | `{ "type": "cursor_absent" }`, `{ "type": "empty_collection", "path" }`, or `{ "type": "response_predicate", "expression" }` |
| `accumulate` | JSONata path collected from every page into one array, available as `steps..accumulated` |
| `max_iterations` | Required. Hard cap on pages, at most 100 |
## Limits
Saving a workflow fails if an API Flow node breaks these limits:
* At most 20 steps per node
* Step IDs must be unique and match the ID format above, and `target_step_id` must name one of them
* Extraction names must be unique within a step
* At most 40 headers and 40 query parameters per step
* `retry.max_attempts` at most 5
* `repeat.max_iterations` is required and at most 100
## Edges
API Flow uses a single `to` edge to the next node.
## Notes
* With the default `credentials: "include"`, requests are sent from the page the browser is on. If that page has a different origin than the API (a different subdomain counts), the request fails. Make sure the [Start](/concepts/workflow-dsl/start) URL or an earlier [Navigate](/concepts/workflow-dsl/navigate) puts the browser on the right origin
* `POST`, `PUT`, `PATCH` and `DELETE` steps change data on the target system on every run. Test them carefully
* If an API Flow node replaces UI steps that did the same thing, remove those steps, or the action runs twice
* The Builder Agent can build API Flow nodes from network traffic it captures while you navigate the site
# Bool Condition
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/bool-condition
Add conditional branching logic to workflows
The **Bool Condition** node creates branching logic in your workflow based on conditions. The workflow follows the `true` or `false` edge depending on the evaluation result.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `execution` | string | Yes | Execution type: `STATIC` (UI: "Static"), `LLM_VISION` (UI: "AI (Screenshot)"), or `PROMPT` (UI: "AI (Context)") |
| `comparison_operator` | string | No | Comparison operator for `STATIC` execution |
| `comparison_value_1` | string | No | First value to compare (for `STATIC` execution) |
| `comparison_value_2` | string | No | Second value to compare (for `STATIC` execution, not needed for `IS_NULL`/`IS_NOT_NULL`) |
| `prompt` | string | No | Natural language condition for `LLM_VISION` or `PROMPT` execution |
| `wait_time` | number | No | Maximum time (ms) to wait before evaluation |
| `clear_cookies_on_false` | boolean | No | Clear cookies if condition evaluates to false |
| `error_on_false_message` | string (UUID) | No | Error code to fail with when the condition evaluates to false, instead of following a `false` edge: an error code `id` from [List error codes](/workflow-api/list-error-codes) or [Create error code](/workflow-api/create-error-code) |
| `max_iterations` | number | No | Iteration cap when a branch loops back to an earlier node. Defaults to `100` for looping conditions; `0` disables the cap |
## Comparison Operators
For `STATIC` execution, use these operators:
| Operator | Description |
| - | - |
| `EQUAL` | Values are exactly equal |
| `NOT_EQUAL` | Values are not equal |
| `CONTAINS` | First value contains the second |
| `NOT_CONTAINS` | First value does not contain the second |
| `STARTS_WITH` | First value starts with the second |
| `ENDS_WITH` | First value ends with the second |
| `IS_NULL` | Value is null or undefined |
| `IS_NOT_NULL` | Value is not null |
## Examples
### Basic Comparison
Check if a value equals a specific string:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Check status",
"action": "BOOL_CONDITION",
"parameters": {
"execution": "STATIC",
"comparison_operator": "EQUAL",
"comparison_value_1": "{{context.status}}",
"comparison_value_2": "active"
}
}
```
### JSONata Expression
Use complex logic with JSONata:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Drug in list?",
"action": "BOOL_CONDITION",
"parameters": {
"execution": "STATIC",
"comparison_value_1": "{{context.drug in [\"Skyrizi\", \"Tremfya\", \"Botox\"]}}",
"comparison_value_2": "true",
"comparison_operator": "EQUAL"
}
}
```
### LLM\_VISION Condition
Use AI to evaluate a visual condition based on a screenshot:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Check if modal is visible",
"action": "BOOL_CONDITION",
"parameters": {
"execution": "LLM_VISION",
"prompt": "Is there a confirmation modal visible on the screen?"
}
}
```
### PROMPT Condition
Use AI to make a decision based on context data (no screenshot):
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Check if order qualifies for discount",
"action": "BOOL_CONDITION",
"parameters": {
"execution": "PROMPT",
"prompt": "Based on the order total {{context.order_total}} and customer status {{context.customer_status}}, does this order qualify for a 10% discount? Orders over $100 from premium customers qualify."
}
}
```
### Clear Cookies on False
Reset session if condition fails. Relevant for login flows to remove stale browser state:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Check session valid",
"action": "BOOL_CONDITION",
"parameters": {
"execution": "STATIC",
"comparison_operator": "NOT_CONTAINS",
"comparison_value_1": "{{context.current_url}}",
"comparison_value_2": "/login",
"clear_cookies_on_false": true
}
}
```
### Loop with an Iteration Cap
When a `true` or `false` edge loops back to an earlier node (retry, while, or pagination loops), use `max_iterations` to bound how many times the condition may run before the workflow gives up:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "More pages to process?",
"action": "BOOL_CONDITION",
"parameters": {
"execution": "STATIC",
"comparison_operator": "EQUAL",
"comparison_value_1": "{{context.has_next_page}}",
"comparison_value_2": "true",
"max_iterations": 25
}
}
```
## Notes
* Use `STATIC` execution with comparison operators for simple, fast evaluations
* Use `LLM_VISION` when you need to evaluate visual conditions based on a screenshot
* Use `PROMPT` when you need AI reasoning on context data without requiring a screenshot
* `max_iterations` only matters when one of the condition's edges loops back to an earlier node. It caps how many times the condition executes across the whole run; when the cap is reached the run fails with `STUCK_LOOP-E0001`. Looping conditions default to `100` if you don't set it — raise it when the loop can legitimately run longer, or set `0` to disable the cap entirely. Forward-only if/else conditions ignore it.
# Captcha
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/captcha
Solve a captcha at a chosen step
The **Captcha** node solves one captcha at this point in the run. It runs the platform's solver for the named captcha whether or not the workflow's automatic captcha solving is on, so it is the way to say "solve here" when automatic solving is off, or when a captcha must be cleared at one specific step, such as right before a submit that only enables once the widget passes. See [Workflow Settings](/concepts/workflow-dsl/workflow-settings) for the automatic solving setting.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `captcha_type` | string | Yes | `turnstile` (Cloudflare Turnstile), `recaptcha_v2`, or `hcaptcha` |
## Examples
### Turnstile before a submit
```json theme={null}
{
"id": "f2a3b4c5-6789-4d01-a234-ef0123456789",
"name": "Solve Turnstile before submit",
"action": "CAPTCHA",
"parameters": {
"captcha_type": "turnstile"
}
}
```
### reCAPTCHA v2
```json theme={null}
{
"id": "0b1c2d3e-4f56-4a78-9b01-c2d3e4f56789",
"name": "Solve reCAPTCHA before login",
"action": "CAPTCHA",
"parameters": {
"captcha_type": "recaptcha_v2"
}
}
```
### hCaptcha after the site's own checkbox
Some sites show their own "I am not a robot" checkbox and open the hCaptcha challenge when it is ticked. Click that checkbox first, then solve:
```json theme={null}
{
"id": "1c2d3e4f-5a67-4b89-8c01-d2e3f4a5b678",
"name": "Solve hCaptcha",
"action": "CAPTCHA",
"parameters": {
"captcha_type": "hcaptcha"
}
}
```
## Notes
* If no captcha of that type is on the page, the node passes and the run continues
* If the captcha is present and cannot be solved, the node fails with error code `CAPTCHA-E0001`, so the workflow's error-code actions (retry, alert, pause) apply
* With automatic captcha solving on, the node is a fast no-op once the captcha is already solved
* Detection finds the widget's iframe or container in the top page only. A captcha on a page embedded in another iframe is not detected, and the node passes as if no captcha were present
* reCAPTCHA v2 is solved by a third-party solver on the worker
* hCaptcha is solved by CloudCruise's own solver. When hCaptcha is on the page but no challenge is showing yet, the node waits up to 15 seconds for one to open, and passes as soon as the widget has a token
* Automatic captcha solving only acts on an hCaptcha challenge that is already open when a step starts. If a site opens hCaptcha from its own checkbox or button, put a Captcha node right after that click
# Click
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/click
Click on elements in the page
The **Click** node performs click interactions on page elements. It supports various click types and execution strategies.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `execution` | string | Yes | Execution type: `STATIC` (UI: "Static") or `LLM_VISION` (UI: "AI (Screenshot)") |
| `selector` | string | No | XPath selector (for `STATIC`) |
| `prompt` | string | No | Natural language description of what to click (for `LLM_VISION`) |
| `click_type` | string | No | Type of click: `click`, `double_click`, `right_click`, or `hover`. Default: `click` |
| `wait_time` | number | No | Maximum time (ms) to wait for the element. Default: 15000 |
| `selector_error_message` | string (UUID) | No | Error code to fail with if the element is not found: an error code `id` from [List error codes](/workflow-api/list-error-codes) or [Create error code](/workflow-api/create-error-code) |
| `human_mode` | boolean | No | If true, use more human-like click behavior |
## Examples
### Basic Click with STATIC Execution
Click a button using an XPath selector:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Click submit button",
"action": "CLICK",
"parameters": {
"execution": "STATIC",
"selector": "//button[@type='submit']",
"wait_time": 10000
}
}
```
### Click with LLM\_VISION
Click an element using natural language:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Click login button",
"action": "CLICK",
"parameters": {
"execution": "LLM_VISION",
"prompt": "Click the blue 'Login' button in the top right corner"
}
}
```
### Double Click
Perform a double-click action:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Open file",
"action": "CLICK",
"parameters": {
"execution": "STATIC",
"selector": "//div[@class='file-item'][1]",
"click_type": "double_click"
}
}
```
### Right Click (Context Menu)
Open a context menu:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Open context menu",
"action": "CLICK",
"parameters": {
"execution": "STATIC",
"selector": "//tr[@data-row-id='item-1']",
"click_type": "right_click"
}
}
```
### Hover
Hover over an element to trigger tooltips or dropdowns:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Hover over menu",
"action": "CLICK",
"parameters": {
"execution": "STATIC",
"selector": "//nav//li[contains(text(), 'Products')]",
"click_type": "hover"
}
}
```
## Notes
* Use `STATIC` execution when possible for speed and reliability
* The `hover` click type is useful for revealing dropdown menus or tooltips before interacting with them
* Add descriptive names to help the maintenance agent understand the purpose of each click
# Delay
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/delay
Wait for a specified duration
The **Delay** node pauses workflow execution for a specified amount of time. This is useful when you need to wait for animations, API responses, or other time-dependent operations.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `delay_time` | number | Yes | Time to wait in seconds |
## Examples
### Basic Delay
Wait for 2 seconds:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Wait for animation",
"action": "DELAY",
"parameters": {
"delay_time": 2
}
}
```
## Notes
* Time is specified in seconds
* Use delays sparingly; prefer using `wait_time` parameters on action nodes when waiting for specific elements
# End
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/end
Exit point of every workflow
The **End** node marks the successful end of a run. When execution reaches it, the run completes and the extracted data is returned in the `execution.success` webhook payload. Every workflow has exactly one End node.
## Parameters
End takes no parameters. Set `parameters` to an empty object.
## Example
```json theme={null}
{
"id": "c3d4e5f6-7890-4abc-def1-234567890abc",
"name": "Done",
"action": "END",
"parameters": {}
}
```
## Edges
End has no outgoing edges. Point the last node of each branch at it:
```json theme={null}
{
"edges": {
"last-node-id": { "to": "end-node-id" }
}
}
```
## Notes
* Several branches can lead to the same End node
* To end a branch with a failure instead, use `error_on_false_message` on a [Bool Condition](/concepts/workflow-dsl/bool-condition) node
* To drop scratch values from the result before the run ends, use a [Transform](/concepts/workflow-dsl/transform) node with `DELETE` operations
# Extract Datamodel
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/extract-datamodel
Extract structured data from the page
The **Extract Datamodel** node extracts structured data from the current page based on a JSON schema. This is useful for scraping data, validating page content, or capturing information for later use in the workflow.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `extract_data_model` | object | Yes | JSON schema defining the data structure to extract |
| `execution` | string | No | Execution type: `STATIC` (UI: "Static"), `LLM_DOM` (UI: "AI (HTML)"), `LLM_VISION` (UI: "AI (Screenshot)"), or `PROMPT` (UI: "AI (Context)"). If omitted in workflow JSON, the runtime treats it as `STATIC` |
| `selector` | string | Conditional | XPath related to extraction. For `STATIC`: optional; used only as a readiness gate (waits until the element appears before extracting) and does **not** scope or filter what is extracted. For `LLM_DOM`: required; defines the DOM subtree whose `outerHTML` is sent to the model. Not used for `LLM_VISION` or `PROMPT` |
| `prompt` | string | Conditional | Additional instructions for LLM extraction. Required for `PROMPT` execution |
| `wait_time` | number | No | Maximum time (ms) to wait for the selector. Default: 15000. Only used when `selector` is provided |
| `selector_error_message` | string (UUID) | No | Error code to fail with if the selector is not found: an error code `id` from [List error codes](/workflow-api/list-error-codes) or [Create error code](/workflow-api/create-error-code) |
| `model` | string | No | Override the default LLM model for extraction. Only used for `LLM_DOM` and `LLM_VISION` |
| `keep_html_metadata` | boolean | No | If true, preserve HTML attributes (classes, IDs, data attributes) when sending to LLM. Default: false (HTML is sanitized). Enable this when you need to extract data from HTML attributes e.g. IDs. Only used for `LLM_DOM` |
### How the `selector` interacts with the datamodel
The `selector` parameter behaves very differently between `STATIC` and `LLM_DOM` — be careful not to confuse the two.
* **`STATIC`**: The `selector` is a readiness gate only. Before extracting, the runtime waits up to `wait_time` ms for the selector to resolve (throwing `selector_error_message` if it never does). It is **not** prepended to field paths and does **not** restrict what is extracted — every `path` inside `extract_data_model` is evaluated against the full document (including iframes and open shadow DOM). To scope `STATIC` extraction to a specific region, put the XPath on the array or field's own `path` instead (e.g., `"path": "//table//tbody/tr"` for row containers with relative `".//td[1]"` children).
* **`LLM_DOM`**: The `selector` is required. The runtime waits for the element, then takes its `outerHTML` and sends only that subtree to the model as the extraction input. Choose the smallest element that still contains every field you want the model to extract.
* **`LLM_VISION`** and **`PROMPT`**: The `selector` is not used.
## Schema Structure
The `extract_data_model` follows JSON Schema with CloudCruise extensions:
```json theme={null}
{
"type": "object",
"properties": {
"field_name": {
"type": "string",
"selected": true,
"description": "Description for LLM extraction",
"path": "//xpath/expression",
"mode": "xpath"
}
}
}
```
### Schema Properties
| Property | Description |
| - | - |
| `type` | Data type: `string`, `number`, `boolean`, `array`, `object` |
| `selected` | Set to `true` to include this field in extraction |
| `description` | Description to help LLM understand what to extract |
| `path` | XPath expression for `STATIC` extraction |
| `mode` | Set to `xpath` for XPath-based extraction |
## Examples
### Basic Extraction with LLM\_DOM
Extract user information using AI:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Extract user details",
"action": "EXTRACT_DATAMODEL",
"parameters": {
"execution": "LLM_DOM",
"extract_data_model": {
"type": "object",
"properties": {
"user_name": {
"type": "string",
"selected": true,
"description": "The user's full name displayed in the header"
},
"email": {
"type": "string",
"selected": true,
"description": "The user's email address"
},
"account_status": {
"type": "string",
"selected": true,
"description": "The account status (active, inactive, pending)"
}
}
}
}
}
```
### STATIC Extraction with XPath
Extract data using explicit XPath selectors:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Extract order details",
"action": "EXTRACT_DATAMODEL",
"parameters": {
"execution": "STATIC",
"extract_data_model": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"selected": true,
"path": "//span[@data-testid='order-id']",
"mode": "xpath"
},
"total_amount": {
"type": "string",
"selected": true,
"path": "//div[@class='total']//span[@class='amount']",
"mode": "xpath"
}
}
}
}
}
```
You can also extract HTML attributes (e.g., `id`, `href`, `data-*`) by pointing the XPath to the attribute:
```json theme={null}
{
"product_ids": {
"type": "array",
"items": {
"type": "string"
},
"selected": true,
"path": "//div[@class='product-card']/@data-product-id",
"mode": "xpath"
}
}
```
## Arrays
### Extract Array of Items
Extract a list of items from the page:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Extract product list",
"action": "EXTRACT_DATAMODEL",
"parameters": {
"execution": "LLM_DOM",
"extract_data_model": {
"type": "object",
"properties": {
"products": {
"type": "array",
"selected": true,
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Product name"
},
"price": {
"type": "string",
"description": "Product price"
},
"sku": {
"type": "string",
"description": "Product SKU"
}
}
},
"description": "List of all products shown in the search results"
}
}
}
}
}
```
### Static Array Extraction
To extract an array using `STATIC` execution, provide an XPath that matches multiple elements. Each matched element becomes an item in the array:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Extract all product names",
"action": "EXTRACT_DATAMODEL",
"parameters": {
"execution": "STATIC",
"extract_data_model": {
"type": "object",
"properties": {
"product_names": {
"type": "array",
"items": {
"type": "string"
},
"selected": true,
"path": "//div[@class='product-card']//h3[@class='product-name']",
"mode": "xpath"
}
}
}
}
}
```
For extracting an array of objects (e.g., table rows with multiple columns), define the `path` on the array to match the repeating container elements, then use relative XPaths for each property within the items:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Extract table rows",
"action": "EXTRACT_DATAMODEL",
"parameters": {
"execution": "STATIC",
"extract_data_model": {
"type": "object",
"properties": {
"orders": {
"type": "array",
"selected": true,
"path": "//table[@id='orders-table']//tbody/tr",
"mode": "xpath",
"items": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"path": "/td[1]",
"mode": "xpath"
},
"customer": {
"type": "string",
"path": "/td[2]",
"mode": "xpath"
},
"amount": {
"type": "string",
"path": "/td[3]",
"mode": "xpath"
},
"status": {
"type": "string",
"path": "/td[4]",
"mode": "xpath"
}
}
}
}
}
}
}
}
```
The array's `path` matches each `
` row, and each property uses a relative XPath to extract the corresponding cell within that row.
### Overwrite Arrays
Arrays are 'append' by default. If you extract into the same array twice e.g. in a loop, new items will be appended. You can override this behavior by adding the array key to the `overwriteArrayKeys` array. Here's an example JSON schema you could use in a ExtractDatamodel node:
```json theme={null}
{
"type": "object",
"properties": {
"organization_names": {
"type": "array",
"items": {
"type": "string"
},
"selected": true,
"description": "A list of all organization names. The organization name is written on top of each card and enclosed by a headline tag"
}
},
"overwriteArrayKeys": [
"organization_names"
]
}
```
## Access Browser Variables
We allow extraction of some browser variables:
* The complete URL the browser agent is on: `{{window.location.href}}`
* The path name of the current URL: `{{window.location.pathname}}`
* The query string of the current URL: `{{window.location.search}}`
Here's an example JSON schema you can use in a ExtractDatamodel node:
```json theme={null}
{
"type": "object",
"properties": {
"current_url": {
"type": "string",
"selected": true,
"path": "{{window.location.href}}",
"mode": "xpath"
},
"path_name": {
"type": "string",
"selected": true,
"path": "{{window.location.pathname}}",
"mode": "xpath"
},
"query_string": {
"type": "string",
"selected": true,
"path": "{{window.location.search}}",
"mode": "xpath"
}
}
}
```
Note that the execution type for this needs to be `STATIC`.
## Extract Raw HTML
You can extract the HTML content of the current page using document variables:
* **Sanitized HTML** (`{{document.sanitized}}`): Extracts a simplified version of the HTML that removes most attributes and only maintains the structure, tags, and content. This is useful for cleaner data extraction and reduces noise when processing HTML.
* **Complete HTML** (`{{document}}`): Extracts the entire raw HTML with all attributes intact, including classes, IDs, data attributes, styles, and other metadata.
Here's an example JSON schema you can use in a ExtractDatamodel node:
```json theme={null}
{
"type": "object",
"properties": {
"sanitized_html": {
"type": "string",
"selected": true,
"path": "{{document.sanitized}}",
"mode": "xpath",
"description": "Clean HTML with structure, tags, and content only"
},
"complete_html": {
"type": "string",
"selected": true,
"path": "{{document}}",
"mode": "xpath",
"description": "Full raw HTML with all attributes"
}
}
}
```
Note that the execution type for this needs to be `STATIC`.
## Extract OS Clipboard
Use the `{{clipboard}}` variable to read the worker's operating-system clipboard as the extraction source. When an application can copy the exact text you need to the clipboard, you can read it back **verbatim**: the full, character-exact content (including multi-line text), without the transcription errors or truncation of screenshot/vision extraction.
First trigger the copy (a **Copy** button via a `CLICK` node, or a [`{{ctrl+c}}` keystroke](/concepts/workflow-dsl/input-text#keystroke-tokens)), then extract `{{clipboard}}`. The extract waits for the clipboard to be populated, so no manual `DELAY` is needed. It also clears the clipboard after reading, so a copy-per-row loop reads each new value rather than a stale prior one, whichever way you copy.
This is especially useful for **native or remote-desktop (RDP/DCV) applications**, where the content isn't part of the page DOM but the app can still copy it to the clipboard.
Here's an example JSON schema you can use in a ExtractDatamodel node:
```json theme={null}
{
"type": "object",
"properties": {
"clipboard_text": {
"type": "string",
"selected": true,
"path": "{{clipboard}}",
"mode": "xpath",
"description": "The current OS clipboard contents"
}
}
}
```
Note that the execution type for this needs to be `STATIC`. If the clipboard can't be read, the node fails with error code `CLIPBOARD-E0001`. If it is still empty after the wait (the copy never landed), it fails with `CLIPBOARD-E0002` rather than returning empty data.
## Notes
* Use `STATIC` execution with XPaths for speed and reliability when page structure is stable
* Use `LLM_DOM` for complex pages or when selectors frequently change
* Add clear descriptions for each field to help the LLM understand what data to extract
* Arrays extracted multiple times (e.g., in a loop) append by default; use `overwriteArrayKeys` to replace
* For `STATIC`, scope row containers via the array's own `path` (not the node `selector`), and prefer relative child paths like `.//td[1]` for cleanest semantics
# Extract Network
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/extract-network
Capture and extract data from network requests
The **Extract Network** node intercepts XHR and Fetch requests made by the page and extracts data from their responses. This is useful when you're looking to extract a lot of data and want to optimize runtime or the data you need isn't rendered in the DOM but is available in API responses.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `url` | string | Yes | URL pattern to match against intercepted requests |
| `extract_data_model` | object | Yes | Data model schema for extracting specific fields using [JSONata](https://jsonata.org/) (also accepts JSONPath-style `$.field` syntax) |
| `selector` | string | No | XPath expression to wait for before extracting network data |
| `wait_time` | number | No | Maximum total time (ms) to wait for both the `selector` (if set) to appear and a matching network request to be captured. The two phases share this budget. Default: 15000 |
| `full_request` | boolean | No | When true, adds full request/response metadata to the extracted data. Default: false |
| `allow_empty_on_no_match` | boolean | No | When true, continues with an empty object if no matching request is captured within `wait_time`. Default: false |
| `api_method` | string | No | HTTP method filter (for example, `GET` or `POST`) applied when matching captured requests |
If no matching request is captured within `wait_time`, the node fails with an error by default rather than proceeding with an empty response. Set `allow_empty_on_no_match` to `true` to continue with `{}` instead. Make sure the URL is pointed to by an `EXTRACT_NETWORK` node and that the page actually fires the request as part of the workflow.
## URL Matching
The node supports exact, substring, and wildcard matching. If there are several matches, the most recent matching request is returned.
### Exact Match
If the provided URL exactly matches one request, the network data of that request is returned.
### Substring Match
If no exact match is found, the node falls back to substring matching. Say you specify `/api/users/` as url, then any request containing `/api/users/` in the URL would be matched and the most recent request would be returned.
### Wildcard Match
Use `*` for dynamic URL segments or suffixes.
```
/api/v1/coverages/*
```
This matches:
* `/api/v1/coverages/123`
* `/api/v1/coverages/456789`
* `/api/v1/coverages/123?include=details`
But **not**:
* `/api/v1/customers/123`
## Full Request
When `full_request` is enabled, complete request and response metadata is added directly inside each extracted property. This is useful when you need to capture authentication tokens, cookies, or other request/response details.
### Default Output (full\_request: false)
```json theme={null}
{
"user_data": [
{
"id": "123",
"name": "John Doe"
}
]
}
```
### With Full Request (full\_request: true)
When `full_request` is enabled, single-element arrays are unwrapped and the extracted data is placed inside `response.body`:
```json theme={null}
{
"user_data": {
"request": {
"url": "https://api.example.com/users/123",
"method": "GET",
"headers": {
"authorization": "Bearer xxx",
"content-type": "application/json"
},
"body": null
},
"response": {
"status": 200,
"statusText": "OK",
"headers": {
"content-type": "application/json"
},
"body": {
"id": "123",
"name": "John Doe"
}
},
"timestamp": "2024-01-15T10:30:00.000Z"
}
}
```
The additional properties added:
* **request.url**: The full request URL
* **request.method**: HTTP method (GET, POST, etc.)
* **request.headers**: Request headers sent with the request
* **request.body**: Request body (for POST/PUT requests)
* **response.status**: HTTP status code
* **response.statusText**: HTTP status text
* **response.headers**: Response headers
* **response.body**: The extracted response data
* **timestamp**: When the request was made
## Examples
### Basic Usage
Extract user data from an API response:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Extract user data",
"action": "EXTRACT_NETWORK",
"parameters": {
"url": "/api/v1/users",
"extract_data_model": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"path": "$.id",
"selected": true
},
"email": {
"type": "string",
"path": "$.email",
"selected": true
}
}
}
}
}
```
### With Wildcard URL Matching
Extract specific fields using a wildcard to match dynamic URLs:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Extract order status",
"action": "EXTRACT_NETWORK",
"parameters": {
"url": "/api/v1/orders/*",
"extract_data_model": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"path": "$.id",
"selected": true
},
"status": {
"type": "string",
"path": "$.order.status",
"selected": true
}
}
}
}
}
```
Common path expressions:
* `$` — root object
* `$.field` or `field` — direct field access
* `$.parent.child` or `parent.child` — nested field
* `$[0]` — first array element
### Wait for Element First
Wait for a specific element to appear before extracting network data:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Extract dashboard data",
"action": "EXTRACT_NETWORK",
"parameters": {
"url": "/api/dashboard/data",
"selector": "//*[@data-loaded='true']",
"wait_time": 20000,
"extract_data_model": {
"type": "object",
"properties": {
"dashboard_data": {
"type": "string",
"path": "$",
"selected": true
}
}
}
}
}
```
## Notes
* The node automatically trims whitespace from URL patterns
* When multiple requests match, the most recent match is used for extraction
* Supported response types: JSON, HTML, and XML. Non-JSON responses are returned as raw strings.
* Network traffic classified as noise (e.g. analytics, tracking pixels, static assets, health checks) is filtered out from extraction candidates by default. If the request you're trying to extract doesn't appear, toggle **Filter noise** off in the Network Traffic panel to see all captured requests.
# File Download
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/file-download
Capture and store downloaded files
The **File Download** node waits for a file download to complete and stores it with metadata. This node does **not** trigger the download itself—the download must be initiated by a previous action (e.g., a Click node on a download button). The exception is when `trigger_print` is set, which triggers the browser's print dialog for PDF generation.
The node polls for a download to complete before timing out. By default, the timeout is 60 seconds, but this can be configured using the `timeout_seconds` parameter.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `metadata` | object | Yes | Metadata to attach to the downloaded file for identification in results |
| `trigger_print` | boolean | No | If true, trigger browser print dialog for PDF generation instead of waiting for a download |
| `continue_on_failed_download` | boolean | No | If true, continue workflow even if no download is detected within the timeout |
| `timeout_seconds` | number | No | Maximum time in seconds to wait for a download to complete. Default: 60. Range: 5-300 |
| `expect_print_dialog` | boolean | No | Immediately send OS-level key sequences to save from a print dialog opened by the preceding action. See [caution below](#expect-print-dialog) |
## Examples
### Capture a Download
First trigger the download with a Click node, then capture it:
```json theme={null}
{
"id": "click-download",
"name": "Click download button",
"action": "CLICK",
"parameters": {
"execution": "STATIC",
"selector": "//button[@id='download-report']"
}
}
```
Then capture the file. You can include workflow context in the metadata:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Capture invoice download",
"action": "FILE_DOWNLOAD",
"parameters": {
"metadata": {
"invoice_id": "{{context.invoice_id}}",
"customer": "{{context.customer_name}}",
"date": "{{context.invoice_date}}"
}
}
}
```
## Expect Print Dialog
`expect_print_dialog` does **not** wait for or detect a print dialog. When enabled, the agent immediately sends OS-level key sequences (e.g. Cmd+S / Ctrl+S, Enter) to save from a print dialog **as soon as the preceding action completes**. It assumes the dialog is already open.
If the print dialog has not appeared — for example, if the preceding Click failed to open it — the keystrokes will be sent to the underlying page. This can cause unintended actions such as clicking buttons, submitting forms, or changing data on the page.
Only enable this when you are certain the previous step (e.g. a Click node) reliably opens a native print dialog. The typical pattern is:
1. **Click node** — clicks a "Print" button that opens the browser's native print dialog
2. **File Download node** with `expect_print_dialog: true` — immediately sends keystrokes to save the printed PDF
If the print dialog does not open reliably, consider adding a Delay node between the Click and File Download, or verifying the dialog appears before enabling this flag.
## Notes
* This node waits for a download. Use a Click node before it to trigger the actual download
* Only **one download** can be captured per File Download node. If multiple downloads are triggered, only the last one is captured
* To capture multiple files, use separate Click → File Download node pairs for each file
* Downloaded files are stored and accessible in the workflow results
* Downloaded files are added to the `file_urls` array in the response payload (available in both the API response and webhook events)
* Each downloaded file triggers a [`file.uploaded`](/concepts/webhooks#file.uploaded) webhook event with the signed file URL and metadata
* The `trigger_print` option triggers the browser print dialog for PDF generation
* Set `continue_on_failed_download: true` for optional downloads that shouldn't fail the workflow
* Downloaded files are limited to **150 MB**. Larger files fail the node with an upload error
* Supported file types: PDF, images (PNG, JPEG, GIF, WebP, TIFF), text, CSV, JSON, XML, Excel (XLS, XLSX), Word (DOCX), and ZIP
* Signed file URLs expire based on your workspace settings (default: 7 days, configurable from 1 hour to 7 days). Configure this in [workspace settings](https://app.cloudcruise.com/settings/data-retention)
* The node times out after the configured `timeout_seconds` (default: 60 seconds) if no download is detected
# File Upload
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/file-upload
Upload files to web forms
The **File Upload** node uploads files to file input fields on web pages. To use it, supply a signed download URL pointing to the file as the `signed_file_url` parameter — CloudCruise fetches the file from that URL at execution time and uploads it to the page. The URL can come from any storage provider (S3, GCS, Azure Blob, etc.) as long as it is publicly retrievable via GET without additional authentication.
The node assumes that the system-level file upload dialog is already opened — add a Click node on the file input trigger before this node.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `signed_file_url` | string | Yes | Signed URL pointing to the file to upload |
| `file_name` | string | No | Custom file name (including extension) to use when uploading. If not provided, a unique name is generated automatically. |
## Examples
### Basic File Upload
Upload a file using a signed URL:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Upload document",
"action": "FILE_UPLOAD",
"parameters": {
"signed_file_url": "{{context.inputs.file_url}}"
}
}
```
### File Upload with Custom Name
Upload a file with a specific file name:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Upload resume",
"action": "FILE_UPLOAD",
"parameters": {
"signed_file_url": "{{context.inputs.file_url}}",
"file_name": "resume.pdf"
}
}
```
You can also use variables for the file name:
```json theme={null}
{
"id": "3f2b8c1e-7a4d-4e9b-9c6f-1d2e3f4a5b6c",
"name": "Upload document",
"action": "FILE_UPLOAD",
"parameters": {
"signed_file_url": "{{context.inputs.file_url}}",
"file_name": "{{context.inputs.file_name}}"
}
}
```
## Notes
* When using file uploads, include the file URL in your workflow inputs
* The file must be accessible via a signed URL (pre-authenticated URL)
* The file node assumes that the system-level file upload dialog is already opened. Add a click node before the file upload node to trigger this.
* Common file types like PDFs, images, and documents are supported
* For multiple file uploads, use a Loop node that loops over an array of file URLs
* Use `file_name` when the target website validates or displays the uploaded file name (found under Advanced settings in the workflow editor)
# Input Select
Source: https://docs.cloudcruise.com/concepts/workflow-dsl/input-select
Select options from dropdown menus
The **Input Select** node selects options from dropdown menus, select elements, and other selection controls.
Native HTML `