Skip to content

Building workflows from blocks

Compose navigation, actions, extraction and control flow into a versioned workflow: nine block types, immutable versions and labels that become variables.

Last updated:

A line is already a graph

login credentialId navigation falls through conditional first match wins extraction schema → run output action nextBlockLabel: navigation matched otherwise finallyBlockLabel runs last, always -- except on cancel a block with no nextBlockLabel falls through in declaration order, so a straight line is just the graph in which every edge is the obvious one
Blocks without a nextBlockLabel fall through in declaration order; explicit edges appear only where the flow branches.

Blocks, versions and labels

A workflow is a list of blocks. When a block has no nextBlockLabel, execution falls through to the next block in declaration order — so a simple sequence is the graph in which every edge is the obvious one, and you only draw an edge when you mean a branch.

Every publish creates a new immutable version, and a run executes the version it started with; publishing mid-run changes nothing already in flight. Block labels must be valid identifiers (Read_heading, not a phrase with spaces) because a label becomes a template variable: a later block can reference {{Read_heading_output.heading}}.

The nine block types

The type column names the field each block is built around.

Name Type Description
navigation url Opens a URL. The simplest edge of the graph.
action instruction An instruction, optionally with a composed-XPath selector. aiFallback chooses whether the model acts proactively or only once the selector fails (proactive|fallback).
extraction instruction Pulls structured data from the page, optionally against a JSON Schema.
login credentialId Signs in with a vault credential. The block carries a credential id, never a secret.
validation criterion Checks a criterion against the live page and fails the run when it does not hold.
code code Customer JavaScript. It runs in the session container, never in the platform process.
http_request method, url Calls an external API from the run and hands the response to later blocks.
loop over Repeats nested blocks over a list, bounded by maxIterations.
conditional branches Branches on criteria. All branch criteria are answered in one model call against one snapshot; the first true branch wins.

Publish, run, poll

Requires: api-key inference

BASE="https://browserberg.com"
AUTH="Authorization: Bearer $BROWSERBERG_API_KEY"
JSON="Content-Type: application/json"

# Publish. Every publish is a new immutable version; labels must be valid
# identifiers because they become template variables.
WFID=$(curl -s -X POST "$BASE/v1/workflows" -H "$AUTH" -H "$JSON" -d '{
  "title": "Read the example heading",
  "blocks": [
    { "blockType": "navigation", "label": "Open_page", "url": "https://example.com" },
    { "blockType": "extraction", "label": "Read_heading",
      "instruction": "the page heading",
      "schema": { "type": "object", "properties": { "heading": { "type": "string" } } } }
  ]
}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["workflow"]["workflowId"])')

SID=$(curl -s -X POST "$BASE/v1/sessions" -H "$AUTH" -H "$JSON" -d '{"ttlSeconds": 300}' \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["session"]["id"])')

RUNID=$(curl -s -X POST "$BASE/v1/workflows/$WFID/runs" -H "$AUTH" -H "$JSON" \
  -d "{\"sessionId\": \"$SID\"}" \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["run"]["runId"])')

# Poll until terminal.
for i in $(seq 1 90); do
  STATUS=$(curl -s "$BASE/v1/workflow-runs/$RUNID" -H "$AUTH" \
    | python3 -c 'import json,sys; print(json.load(sys.stdin)["run"]["status"])')
  if [ "$STATUS" != "running" ]; then break; fi
  sleep 2
done

echo "run: $STATUS"
curl -s "$BASE/v1/workflow-runs/$RUNID" -H "$AUTH" \
  | python3 -c 'import json,sys; print(json.dumps(json.load(sys.stdin)["run"]["outputs"], indent=2))'

curl -s -X DELETE "$BASE/v1/sessions/$SID" -H "$AUTH" > /dev/null

Sequencing, cleanup, validation

For portals that permit one session per login, set runSequentially with a sequentialKey: runs sharing the key queue up instead of evicting each other's sign-in. finallyBlockLabel names a block that runs last on every completed or failed run — with one deliberate exception: it does not run on a cancel, because a cancel means stop now, not stop after more steps.

When you publish something invalid, the API returns all validation errors at once, each with a code and a position — one round trip to fix the whole definition, not one per mistake.

Stylised view of a Browserberg workflow with blocks connected in sequence and one branch
Versions are immutable; a running workflow finishes on the version it began with.

Keep building

  • Workflow healing When the portal changes under a working workflow
  • Schedule triggers Run the workflow every night at 02:30
  • Workflows reference Every block field, spelled out