Skip to main content

Writing workflows

Syntax​

# Start browser and navigate
StartWebBrowser("https://example.com")

# Navigate to another page
NewChangeUrlBrowserTab("https://example.com/products")

# With output variable
products = GetHtmlElements("//div[@class='item']")

# Keyword arguments
response = HttpRequest("https://api.example.com/data", method="POST")

# Activity name & description — set only when intent isn't obvious from type + inputs
# (above all raw-selector clicks; skip self-describing steps like Navigate/HttpRequest)
# _name: 2-3 word action label of what the step achieves (not the activity type)
# _description: what it achieves, the UI/state it assumes, and how you'd know it failed
# These are metadata (not inputs); they power automated failure diagnosis.
LeftClick("//button[@id='save']", _name="Save record",
_description="Submits the form; assumes the dialog is open. Fails if validation errors remain.")

# Variable references (no quotes)
WriteToExcel("output.xlsx", "Sheet1")

# Control flow
if items > 0:
for item in items:
WriteLine(item)
WriteToExcel("results.xlsx", "Results")
else:
WriteLine("No items found")

# Error handling
try:
ClickElement("//button[@id='submit']")
except Exception as err:
TakeScreenshotWeb("./errors", "error")

# Groups
with Group("Login Steps"):
StartWebBrowser("https://app.example.com/login")
TextInput("//input[@name='email']", "user@example.com")
ClickElement("//button[text()='Sign In']")

# Retry a block of steps as a group on failure
with RetryOnError(retries=3, delay=1):
response = HttpRequest("https://api.example.com/data")
WriteLine(response)

Core Activities​

ActivitySignatureOutput
GetExecutionInfo()outputVariable
SetOutcome(outcome)-
WriteLine(text)-
HttpRequest(url, method="GET", body=..., extraHeaders=..., multipartNames=..., multipartData=..., debugInput=False)outputVariable
DownloadFile(url, extraHeaders=..., outputPath=...)outputVariable
ChatGPT(input, gptModel="gpt-3.5-turbo", maxTokens=4096, responseType="text")outputVariable
Regex(pattern, text)outputVariable
Delay(timer)-
Group()-
Assign(variables)-
LoadEnvVariables(environment)-
SetEnvironmentVariable(environmentId, variableName, deleteVariable=..., isSecret=False)outputVariable
ArrayAppend(array, elements)-
Watchdog(directoryPath, fileExtension, newFileVariable)-
RetryOnError(retries=1, delay, isRetrying=...)outputVariable
GenerateUniqueId()outputVariable
GetIntegerRandomNumber(lowerLimit=..., upperLimit=...)outputVariable
RunPython(code, environment=..., interpreterPath=...)outputVariable
SetupPythonEnvironment(githubToken=..., githubUrl=..., branch=..., repoPath=..., venvPath, requirementsPath=..., interpreterPath=...)pythonPath
RunPythonFile(filePath, interpreterPath, environment=..., args=...)stdout
Test(workflowId, parameters=...)outputVariable
Assert(condition="")outputVariable
LaunchApplication(fullPath, arguments=..., workingDirectory=..., runAsAdmin=...)outputVariable
TerminateApplication(processIdentifier, forceTerminate=...)outputVariable
IfCondition(condition="")-
While(condition="")-
ForEachLoop(collection="", iteratorName=..., iteratorIndex=...)-
TryCatch()outputVariable
Break()-
Continue()-
ExecuteChildWorkflow(workflowId, browserSessionStrategy="useCurrentBrowserSession", profilePath=..., fireForget=..., parameters=..., outputMappings=...)-
EndExecution(endMode="complete", errorMessage=...)-
TimeoutExecution(endMode="terminate", timeout=1440)-
SendSignal(workflowId, runId=..., data=...)-
ReceiveSignal(timeoutMinutes=...)-
LiveInput(variables=..., timeoutMinutes=...)-
SMTPEmail(useOwnCredentials=..., emailRecipient, emailSubject, emailBody, cc=..., bcc=..., attachmentsFilepath=...)-
OpenFile(filePath)-
ReadFileContent(filePath)outputVariable
CreateDirectory(path, failIfExists=False)-
DeleteDirectory(path, failIfMissing=False)-
SaveToFile(content, filePath, createDirectories=False)-
SaveImage(source, filePath, createDirectories=False)-
WriteToExcel(fullPath, sheetName, skipRow, writeExisting=..., rows=...)outputVariable
DeleteExcelRow(filePath, sheetName=..., deleteRow)-
ExtractDataExcel(filePath, sheetName=..., includeHiddenSheet, isCSV=False)outputVariable
DatabaseToExcel(dbTable="", directoryPath, optionalName)outputVariable
FileExists(document)outputVariable
FileCopy(document, location, delete=..., overwrite=...)outputVariable
FileDelete(location)-
FileRename(document, name)outputVariable
GetFolderFiles(folderPath)outputVariable
CombinePDFs(location, name, delete=..., candidates)outputVariable
SplitPDF(location, locationPages)outputVariable
ExtractPDFText(filePath)outputVariable
ExtractPDFTable(filePath, headers=..., mergeGap=..., maxRowGap=..., anchorColumn=..., requireAnyColumn=...)outputVariable
EditPDF(filePath, outputPath=..., replacements)outputVariable
MergePDFs(sources, outputPath, maxSize=..., oversizeBehavior="shrink")outputVariable
WatermarkPDF(filePath, imagePath, outputPath=..., skipFirstPage=False, pages=..., rotation=45, opacity=0.15, scale=0.85)outputVariable
UploadResource(filePath, resourceName, resourceType=..., description=..., tags=..., folder=...)outputVariable
DownloadResource(resourceId=..., resourceName=..., outputPath)outputVariable
ListResources(names=..., folder=..., resourceType=..., tags=..., limit=...)outputVariable
GetResourceInfo(resourceId=..., resourceName=...)outputVariable
DeleteResource(resourceId=..., resourceName=...)outputVariable
UpdateResource(resourceId=..., resourceName=..., resourceType=..., description=..., tags=..., folder=...)outputVariable
ReplaceAll(stringToProcess, toReplace=..., replaceWith=...)outputVariable
OCR(filePath)outputVariable
OCRText(projectId, text)outputVariable
TextSimilarity(textCompare, threshold=0.93, candidates)outputVariable
KeyDataExtractor(projectId, document)outputVariable
ExcelToTable(filePath, sheetName, sheetIndex=..., rowToSkip=1)outputVariable
ParseJson(text)outputVariable
JsonToString(variable)outputVariable
OpenDatabaseConnection(connectionString=..., databaseType="postgres")outputVariable
CloseDatabaseConnection(db)-
RunQuery(db, query)outputVariable
RunNonQuery(db, query)-
StartWebBrowser(url, browserSessionStrategy="useCurrentBrowserSession", profilePath=..., playwrightDownloadPath=...)-
AcceptPrompt(acceptMessage=...)-
DenyPrompt()-
NewChangeUrlBrowserTab(url)-
SwitchBrowserTab(url)-
RemoveBrowserTab(url)-
ReloadCurrentPage()-
WaitForElement(selector, iframeSelector=..., iframeSelectors=..., state="visible", timeout=30000)-
WaitForPageToLoad(seconds)-
TakeScreenshotWeb(directoryPath, optionalName)outputVariable
WebpageToPDF(filePath, format="A4", landscape=False, width=..., height=..., margin=..., scale=..., printBackground=False, preferCSSPageSize=False, media="print")outputVariable
ExecuteJavaScript(script, selector=..., iframeSelector=..., iframeSelectors=..., args=..., timeout=...)outputVariable
ClickElement(selector, iframeSelector=..., iframeSelectors=..., useBrowserDebugger=...)-
DoubleClickElement(selector, iframeSelector=..., iframeSelectors=..., useBrowserDebugger=...)-
TextInput(selector, iframeSelector=..., iframeSelectors=..., text=..., deletePrevious=..., writeSequentially=...)outputVariable
DropDownSelect(selector, iframeSelector=..., iframeSelectors=..., text=..., index=...)-
FormSubmit(selector, iframeSelector=..., iframeSelectors=...)-
AttachFile(selector, iframeSelector=..., iframeSelectors=..., filePath)-
MouseOver(selector, iframeSelector=..., iframeSelectors=...)-
WebElementFocus(selector, iframeSelector=..., iframeSelectors=...)-
SelectText(selector, selectorStart, selectorEnd)-
PageScroll(xcoordinate, ycoordinate)-
ScrollIntoView(selector, iframeSelector=..., iframeSelectors=...)-
CheckHtmlElement(selector, iframeSelector=..., iframeSelectors=..., timeout=5000)outputVariable
GetHtmlElementProperty(selector, iframeSelector=..., iframeSelectors=..., property)outputVariable
GetHtmlElements(selector, iframeSelector=..., iframeSelectors=...)outputVariable
GetHtmlChildren(selector, iframeSelectors=...)outputVariable
GetHtmlDataFromAttribute(selector, attribute, iframeSelectors=...)outputVariable
GetHtmlElementPosition(selector, iframeSelector=..., iframeSelectors=...)outputVariable
KeyDown(selector, iframeSelector=..., iframeSelectors=..., key)-
KeyUp(selector, iframeSelector=..., iframeSelectors=..., key)-
KeyDownUp(selector, iframeSelector=..., iframeSelectors=..., key=...)-
MouseDown(selector, iframeSelector=..., iframeSelectors=..., button="left")-
MouseUp(selector, iframeSelector=..., iframeSelectors=..., button="left")-
LoseFocus(selector, iframeSelector=..., iframeSelectors=...)-
TakeScreenshotDesktop(directoryPath=..., optionalName=...)outputVariable
TakeScreenshot(elementTargetMethod="xPath", xPath=..., xOffset=..., yOffset=..., checkOrphanedElements=..., disableGdi=..., referenceText=..., textMatchDistance=..., matchThreshold=..., indexInFoundItems=..., ocrProvider=..., referenceImage=..., expandX=..., expandY=..., elementScreenshotFilePathName=...)outputVariable
LeftClick(elementTargetMethod="xPath", xPath=..., xOffset=..., yOffset=..., checkOrphanedElements=..., useOsMouseForClick=..., referenceText=..., textMatchDistance=..., matchThreshold=..., indexInFoundItems=..., ocrProvider=..., referenceImage=...)-
LeftDoubleClick(elementTargetMethod="xPath", xPath=..., xOffset=..., yOffset=..., checkOrphanedElements=..., referenceText=..., textMatchDistance=..., matchThreshold=..., indexInFoundItems=..., ocrProvider=..., referenceImage=...)-
RightClick(elementTargetMethod="xPath", xPath=..., xOffset=..., yOffset=..., checkOrphanedElements=..., referenceText=..., textMatchDistance=..., matchThreshold=..., indexInFoundItems=..., ocrProvider=..., referenceImage=...)-
MouseHover(elementTargetMethod="xPath", xPath=..., xOffset=..., yOffset=..., checkOrphanedElements=..., referenceText=..., textMatchDistance=..., matchThreshold=..., indexInFoundItems=..., ocrProvider=..., referenceImage=...)-
MouseWheel(elementTargetMethod="xPath", xPath=..., xOffset=..., yOffset=..., checkOrphanedElements=..., referenceText=..., textMatchDistance=..., matchThreshold=..., indexInFoundItems=..., ocrProvider=..., referenceImage=..., wheelDelta=...)-
Drag(elementTargetMethod="xPath", xPath=..., xOffset=..., yOffset=..., checkOrphanedElements=..., referenceText=..., textMatchDistance=..., matchThreshold=..., indexInFoundItems=..., ocrProvider=..., referenceImage=..., deltaX=..., deltaY=...)-
KeyboardInput(elementTargetMethod="xPath", xPath=..., xOffset=..., yOffset=..., checkOrphanedElements=..., referenceText=..., textMatchDistance=..., matchThreshold=..., indexInFoundItems=..., ocrProvider=..., referenceImage=..., keysToSend, writeSequentially=...)-
GetText(xPath, checkOrphanedElements=False)outputVariable
GetDetails(xPath, checkOrphanedElements=False)outputVariable
GetChildren(xPath, propertyName="text", checkOrphanedElements=False)outputVariable
SetClipboard(text)-
GetClipboard()outputVariable
CheckElementExists(xPath, checkOrphanedElements=False)outputVariable
GetToggleState(xPath, checkOrphanedElements=False)outputVariable
ToggleElement(xPath, checkOrphanedElements=False)outputVariable
SetElementValue(xPath, keysToSend=..., confirmWithEnter=False, deselectWithArrowRight=False, selectAllKeys=..., copyKeys=..., pasteKeys=..., checkOrphanedElements=False)-
GetElementValue(xPath, checkOrphanedElements=False)outputVariable
SelectComboboxItem(xPath, comboboxItemText, checkOrphanedElements=False)-
GetSelectedItem(xPath, checkOrphanedElements=False)outputVariable
ExpandElement(xPath, checkOrphanedElements=False)-
CollapseElement(xPath, checkOrphanedElements=False)-
DesktopScrollIntoView(xPath, checkOrphanedElements=False)-
GetTableRowCount(xPath, checkOrphanedElements=False)outputVariable
GetTableColumnCount(xPath, checkOrphanedElements=False)outputVariable
GetTableCell(xPath, tableRow, tableColumn, checkOrphanedElements=False)outputVariable
GetDesktopTableRow(xPath, tableRow, checkOrphanedElements=False)outputVariable
GetTableRows(xPath, checkOrphanedElements=False)outputVariable
GetTableColumnHeaders(xPath, checkOrphanedElements=False)outputVariable
GetTableCellByHeaders(xPath, tableRowHeader, tableColumnHeader, checkOrphanedElements=False)outputVariable
SetTableCellValue(xPath, tableRow, tableColumn, valueToSet, checkOrphanedElements=False)-
SetTableCellValueByHeaders(xPath, tableRowHeader, tableColumnHeader, valueToSet, checkOrphanedElements=False)-
SendNtlmEmail(host=..., port=..., username=..., password=..., enableSsl=..., useDefaultCredentials=False, ignoreCertificateErrors=False, from, to=..., cc=..., bcc=..., subject=..., body=..., attachments=..., sendEmailAsHtml=..., bodyTemplateFilePath=..., tokens=...)-
Office365(permissionsType="application", emailAddress=..., clientSecret=..., clientId, tenantId)-
GetRange(workbookPath=..., worksheetName=..., rangeAddress=...)outputVariable
WriteCell(workbookPath=..., worksheetName=..., rangeAddress=..., values=...)-
AddTableRow(workbookPath=..., worksheetName=..., tableName=..., useSpecificRow=..., values=...)-
SubscribeRequest(notificationUrl)outputVariable
GetMessagesById(messageId=..., includeAttachments=...)outputVariable
MoveFile(filePath, targetPath=...)outputVariable
GetTable(workbookPath=..., worksheetName=..., tableName=...)outputVariable
GetTableRow(workbookPath=..., worksheetName=..., tableName=..., index=...)outputVariable
Microsoft365Email(fromAddress, toRecipients, emailSubject, emailBody, ccRecipients=..., bccRecipients=..., attachments=...)outputVariable
MicrosoftDownloadFile(filePath, targetPath=...)outputVariable
MicrosoftUploadFile(filePath, targetPath=...)outputVariable
SimpleAntiCaptcha(filePath, apiKey)outputVariable
Simple2captcha(filePath, apiKey)outputVariable
V2RecaptchaAnticaptcha(pageURL, siteKey, isInvisible=..., dataSValue=..., apiKey)-
V2Recaptcha2captcha(pageURL, siteKey, apiKey)-
GoogleDriveUploadFile(filePath, targetPath=...)outputVariable
GoogleDriveDownloadFile(fileName, downloadPath, targetPath=...)outputVariable
GoogleDriveDeleteFile(fileName, targetPath=...)outputVariable
GoogleDriveReadSpreadsheet(spreadsheetId, range)outputVariable
GoogleDriveWriteSpreadsheet(spreadsheetId, range, values)outputVariable
GmailSendEmail(to, subject, body)-
LoadNamedTable(tableName, as=...)outputVariable
SaveNamedTable(tableName, createNew=False, mode="upsert")outputVariable
CreateTable(tableName, schema=...)outputVariable
SetTableKeyColumns(tableName, keyColumns)outputVariable
TablePushMany(tableName, rows, upsert=False)outputVariable
TableUpsertRow(tableName, row, append=False)outputVariable
TableDeleteRow(tableName, keys)outputVariable
TableMerge(targetTable, sourceTable, upsert=False)outputVariable
TableQuery(tableName, outputTable=..., filters=..., orderBy=..., limit=..., columns=...)outputVariable
TableLookup(tableName, keys)outputVariable
TableMatch(tableName, criteria)outputVariable
TableImport(tableName, filePath, keyColumns=..., format="auto", sheetName="Sheet1", sheetIndex=..., rowToSkip=1, delimiter=",")outputVariable
TableExport(tableName, filePath, format="csv", delimiter=",", sheetName="Sheet1")outputVariable
DropTable(tableName)-
ApplyRuleSet(tableName, ruleSetName)outputVariable
ExportWithTemplate(tableName, templateName, outputPath)outputVariable
CallFunction(functionName, arguments=...)-
InvokePrimitive(primitiveId, input=..., context=...)outputVariable

Variables​

  • Output: result = HttpRequest("https://...") stores response in result
  • Simple reference (bare name, no quotes): WriteLine(response) — passes the variable response
  • Complex expression (property access, arithmetic, etc.): WriteLine("{{ response.body }}") — use {{ }} inside a quoted string
  • NEVER use ${variable} syntax — that is not valid DSL
Use caseDSL syntax
Pass a variableWriteLine(myVar)
String literalWriteLine("hello world")
Property access / expressionWriteLine("{{ myVar.property }}")
ArithmeticWriteLine("{{ count + 1 }}")

For anything beyond simple property access — filtering, mapping, sorting, pattern matching, or registered function calls — see Expressions. Most RunPython blocks for list transforms or string manipulation are a single expression.

Workflow Options​

# Set via create_workflow / update_workflow options field:
{
"headless": true,
"timeout": 60000,
"retryOnError": true,
"maxRetries": 3,
"expressionLanguage": "expr"
}

expressionLanguage is what the contents of {{ ... }} are written in, for the whole workflow: "expr" (expr-lang, the default and what a workflow that omits the key means), "js" (JavaScript, on goja) or "python" (a Python dialect, on Starlark — not CPython). Read it before writing any expression — the three share the strings, math, fuzzy, collections and path packages and nothing else, so an expression written for the wrong one stores fine and never evaluates.

Webhook Payload Contracts​

When a workflow is triggered via /api/webhooks/{workflow_id}/{user_id}, the full JSON body posted by the caller is available as the webhookBody variable. The shape depends on the source — do not guess, check the contract below.

Microsoft Graph change notifications (Outlook, OneDrive, SharePoint, etc.)​

Microsoft Graph wraps notifications in a value array. Even a single email change arrives as a one-element array. webhookBody IS that envelope:

{
"value": [
{
"changeType": "created",
"resource": "Users/<user-id>/Messages/<message-id>",
"resourceData": {
"@odata.type": "#Microsoft.Graph.Message",
"id": "<message-id>"
},
"subscriptionId": "<sub-id>",
"clientState": "<secret>",
"tenantId": "<tenant-id>",
"subscriptionExpirationDateTime": "2026-04-07T16:30:48+00:00"
}
]
}

Correct DSL expressions:

# Iterate every notification in the batch (Graph may coalesce several):
for notification in webhookBody['value']:
Assign({'messageId': '{{ notification["resourceData"]["id"] }}'})
emailData = GetMessagesById(messageId=messageId, includeAttachments=True)

# Or index the first one directly if you only expect one:
Assign({'messageId': '{{ webhookBody["value"][0]["resourceData"]["id"] }}'})

Incorrect — these will resolve to undefined:

webhookBody["resourceData"]["id"] # MISSING the value[0] envelope — broken
webhookBody["id"] # wrong level entirely

Microsoft Graph lifecycle events​

Same {"value": [...]} envelope, but each item has lifecycleEvent instead of changeType. Lifecycle events are routed to a separate endpoint (/api/webhooks/lifecycleEvent/{workflow_id}) and handled by the platform — workflows rarely need to read them directly.

Generic / custom webhooks​

webhookBody is the raw parsed JSON body as posted, with no envelope added by the platform. If a caller posts {"orderId": 42}, then webhookBody["orderId"] is 42. If a caller posts {"data": {"orderId": 42}}, then you must write webhookBody["data"]["orderId"]. Always confirm the sender's exact shape.

Tips​

  • Browse the activity reference by category
  • Each activity page lists its full input and output schema
  • The designer validates a workflow before it saves
  • Start from a recipe for a working pattern