Introduction
BAL (Browser Automation Language) is a domain-specific language for browser automation. It fully decouples "find element" from "do what": the language only describes intent, and an executor translates it into Playwright calls.
Core goals
- Site-agnostic: no site logic is built in; site differences live in .page configs.
- Input-first compatibility: every input falls back through strategies + verification; fill is never assumed.
- Full upload coverage: images, video, attachments, directories, drag, paste, chunked — seven paths.
- Tags abstracted: "type + suggest + select + dedup + verify" wrapped in one command.
- Extensible: commands, selector strategies and upload strategies extend via registry without core changes.
Parameter declaration
Declare an input: key / label / type / required flag; injected and validated at runtime.
@input · Input
# @input prompt 提示词 text 必填Declare an input: key / label / type / required flag; injected and validated at runtime.
@output · Output
# @output images 图片列表 textDeclare an output: key / label / type; results go to the work log and can feed the next node.
Design principles
| Principle | Description |
|---|---|
| Site-agnostic | No site logic built in; differences go in .page configs |
| Input-first compatibility | Every input falls back through strategies + verification; fill is never assumed |
| Explicit waiting | No blind sleep; wait_until real states |
| Verifiable | Every write op supports verify:true; degrade instead of staying silent |
| Extensible | Commands, selector & upload strategies extend via registry |
| Readable & diffable | Plain-text line syntax with comments and modules |
Contents
1. Lexical rules
Basic units
- 一行一条命令,# 开头为注释
- 命令名小写蛇形:wait_until、set_input_files
- 字符串双引号,支持 \" \\ \n \t 转义
- 参数形式:位置参数 "值"、键值参数 key:value、选项 [opt=val]
- 变量 $name,常量 UPPER_CASE 约定为全局
Comments & blank lines
# 整行注释
goto "https://example.com" # 行尾注释Line continuation
# 长命令用 \ 续行
upload css:"input[type=file]" \
files:"a.jpg","b.png" \
mode:auto \
verify:trueString interpolation
set $name = "世界"
echo "你好,${name}" # 你好,世界
echo "你好,$name" # 同上,简单变量可省花括号2. Selector syntax
Uniform form: <strategy>:<value>[modifiers]
Strategy table
| Strategy | Example | Playwright mapping |
|---|---|---|
| text | text:"登录" | get_by_text |
| role | role:button["提交"] | get_by_role |
| label | label:"邮箱" | get_by_label |
| placeholder | placeholder:"搜索" | get_by_placeholder |
| testid | testid:"submit-btn" | get_by_test_id |
| title | title:"关闭" | get_by_title |
| alt | alt:"封面" | get_by_alt_text |
| css | css:".btn.primary" | locator |
| xpath | xpath:"//button[@type='submit']" | locator |
| nth | nth:3 | 4th in current context |
| ref | ref:$saved | Reference a previously saved element handle |
Modifiers
click text:"删除" [nth=2] # 第 3 个匹配
click text:"删除" [last]
click text:"删除" [first]
click text:"删除" [visible=true] # 只匹配可见元素
click css:".item" [filter="含关键词"]
click role:button["提交"] [exact=true]Chained locating
# 在某个容器内查找
click css:".modal" >> text:"确认"
# 父子、兄弟关系
click css:".form" > placeholder:"用户名"
click text:"用户名" + css:".error"Saving element handles
find placeholder:"标题" -> $title_input
input $title_input value:"测试"3. Variables & expressions
Assignment
set $count = 0
set $url = $BASE + "/page/" + $count
set $pwd = env("APP_PASSWORD")
set $user = input("请输入用户名")
set $now = now()
set $id = uuid()
set $n = random(1, 100)Built-in functions
| Function | Description |
|---|---|
| now([format]) | Current time |
| uuid() | UUID v4 |
| random(min, max) | Random integer |
| env(name) | Environment variable |
| input(prompt) | Runtime prompt |
| file(path) | Read file content |
| glob(pattern) | File glob |
| trim(s) / lower(s) / upper(s) | String utils |
| split(s, sep) / join(list, sep) | List utils |
| len(list) / index(list, i) | List ops |
| json(str) / to_json(obj) | JSON utils |
| base64_encode(s) / base64_decode(s) | Encoding |
| sleep(ms) | Pause (in expressions) |
Expressions
set $total = $price * $qty
set $ok = $count > 3 and $name != ""
set $msg = "共 " + str($count) + " 条"支持:+ - * / %、== != > < >= <=、and or not、括号。
4. Command overview
【配置与会话】 config session context page viewport user_agent locale timezone
【导航】 goto back forward reload open switch_tab close_tab wait_for_load
【查找】 find find_all exists count
【输入】 input fill type type_slow paste clear press press_seq ime_input
【点击】 click dblclick right_click hover focus blur tap
【表单】 check uncheck select select_multi set_date set_time set_range
【上传】 upload upload_dir upload_drag upload_paste upload_chunked cancel_upload
【标签】 tag_add tag_remove tag_select tag_clear tag_list
【鼠标】 drag drag_to scroll_to scroll_by scroll_into_view
【键盘】 keyboard_press keyboard_type keyboard_down keyboard_up
【等待】 wait wait_until wait_for_url wait_for_response wait_for_download
【断言】 assert assert_exists assert_text assert_url assert_count
【提取】 get_text get_attr get_value get_count get_url get_title get_html
【流程】 if/elif/else/endif loop/endloop foreach/endforeach while/endwhile
break continue return
【函数】 func/endfunc call import
【错误】 try/catch/finally on_error retry
【弹窗】 dialog_accept dialog_dismiss dialog_handle
【框架】 frame frame_exit frame_main
【窗口】 new_window switch_window close_window
【网络】 intercept mock block unblock route unroute
【存储】 storage_save storage_load cookie_set cookie_get cookie_clear
【调试】 screenshot echo log pause trace_start trace_stop dump5. Config & session
config (global)
config timeout=30000
config screenshot_on_error=true
config input_mode=auto # auto | fast | compat | human
config input_delay=30
config input_verify=true
config input_max_retry=2
config upload_timeout=180000
config headless=false
config slow_mo=0session
session new
session load "account.json"
session save "account.json"
session closecontext / page
context new [viewport=1920x1080] [locale=zh-CN] [timezone=Asia/Shanghai]
context close
page new
page close
page switch 0
page list -> $pagesviewport / user_agent / locale
viewport 1440 900
user_agent "Mozilla/5.0 ..."
locale "zh-CN"
timezone "Asia/Shanghai"
geolocation 31.23 121.47
permissions ["geolocation", "notifications"]7. Find & existence
find <selector> -> $var
find_all <selector> -> $list
exists <selector> -> $bool
count <selector> -> $nfind placeholder:"标题" -> $title
find_all css:".item" -> $items
exists text:"加载更多" -> $has_more
count css:".comment" -> $n8. Input familyCore
Unified entry: input
input <selector> value:"..."
mode:auto|fast|type|type_slow|keyboard|native|richtext|ime
delay:30
clear:true
verify:true
on_fail:abort|continue|retry(3,1000)Mode reference
| mode | Behavior | Use case |
|---|---|---|
| fast | Direct fill | Static forms, uncontrolled |
| type | press_sequentially(delay) | Plain React components |
| type_slow | Char by char, delay≥100 | Search boxes with suggestions |
| keyboard | click + Ctrl+A + Delete + keyboard.type | Strict anti-bot, IME |
| native | Native setter + dispatch input/change | React/Vue controlled |
| richtext | contenteditable chain | Rich text editors |
| ime | Dispatch composition events | CJK IME |
| auto | Detect type → fallback chain → verify | Recommended default |
Auto-fallback flow
input mode:auto
├─ 检测元素类型
│ ├─ input/textarea → 表单策略链
│ ├─ contenteditable → 富文本策略链
│ └─ 未知 → 先试表单,再试富文本
├─ 表单策略链:fill → press_sequentially → keyboard.type → native_setter
├─ 富文本链:press_seq → insert_text → execCommand → innerText
├─ 每步验证:input_value() 或 inner_text()
├─ 失败降级,全部失败 → 截图 + trace + on_fail
└─ 返回实际生效策略名(写入日志)Dedicated commands
fill placeholder:"标题" value:"测试"
type placeholder:"标题" value:"测试" delay:50
type_slow placeholder:"搜索" value:"Playwright" delay:150
paste css:".editor" value:"很长的正文..."
clear placeholder:"标题"
press css:".editor" key:"Enter"
press css:".editor" key:"Control+A"
press_seq css:".editor" value:"逐字符输入" delay:30
ime_input css:".editor" value:"中文输入法内容"Auto-unlock read-only & disabled
# mode:auto 检测到 readOnly/disabled 时自动执行:
el.readOnly = false; el.disabled = false;
unlock placeholder:"只读字段"
lock placeholder:"字段"Input verification
input placeholder:"标题" value:"测试" verify:true
assert_value placeholder:"标题" equals "测试"
assert_value placeholder:"标题" contains "测"9. File uploadCore
Unified entry: upload
upload <selector>
files:"a.jpg","b.png"
mode:auto|input|drag|paste|chunked|dir
cover:"a.jpg"
verify:true
timeout:180000
on_fail:abort|continue|retry(3,1000)Mode reference
| mode | Behavior | Use case |
|---|---|---|
| input | Direct set_input_files | Standard input[type=file] |
| drag | Simulate drag onto target area | No input; pure drag upload |
| paste | Simulate pasting files | Editors supporting paste upload |
| chunked | Chunked upload for big files | Video, large attachments |
| dir | Upload a whole directory | webkitdirectory |
| auto | Detect → fallback chain | Recommended default |
Fallback chain
upload mode:auto
├─ 查找 input[type=file]
│ ├─ 存在且 visible/attached → set_input_files
│ └─ 隐藏 → 直接 set_input_files(无需可见)
├─ 无 input → 点击上传触发区 → 等待 input 出现 → set_input_files
├─ 仍无 input → 尝试 drag 策略
├─ 仍失败 → 尝试 paste 策略
├─ 大文件(>50MB)→ 自动切 chunked
├─ 等待上传完成:缩略图数量 / 进度条消失 / 网络空闲
└─ 校验:文件数量、封面、缩略图存在Image upload
upload css:"input[type=file]" files:"cover.jpg","detail1.png","detail2.png"
upload css:"input[type=file]" files:glob("assets/*.jpg")
upload css:".upload-area" files:"cover.jpg" mode:drag
upload css:".editor" files:"screenshot.png" mode:paste
upload css:"input[type=file]" files:"a.jpg" cover:"a.jpg" verify:trueVideo upload
upload css:"input[type=file]" files:"video.mp4" mode:auto timeout:600000
upload css:"input[type=file]" files:"video.mp4" mode:chunked chunk_size:5242880
assert_uploaded css:".video-item" count:1
wait_until text:"视频上传完成" timeout:600000Attachment upload
upload css:"input[type=file]" files:"doc.pdf","sheet.xlsx"
upload css:"input[type=file]" files:"archive.zip" verify:trueDirectory upload
upload_dir css:"input[type=file]" dir:"assets/images"Drag · paste · chunked
upload_drag css:".drop-zone" files:"cover.jpg"
drag_file css:".drop-zone" path:"cover.jpg"
upload_paste css:".editor" path:"screenshot.png"
upload_chunked css:"input[type=file]" path:"big-video.mp4" chunk_size:5242880Upload control
cancel_upload # 取消当前上传
pause_upload # 暂停(若页面支持)
resume_upload
assert_uploaded <selector> count:<n>
wait_upload_complete timeout:18000010. Tags & hashtagsCore
tag_add
tag_add <selector>
value:"Playwright"
mode:auto|type|select|enter|comma
wait_suggest:true
select:nth(0)|first|exact|contains
verify:true
dedup:trueInteraction flow
tag_add mode:auto
├─ 点击标签输入入口(或 # 触发)
├─ 输入关键词(走 input mode:auto)
├─ 等待联想候选出现
├─ 选择候选:
│ ├─ select:first → 第一个
│ ├─ select:nth(2) → 第三个
│ ├─ select:exact → 文本完全匹配
│ └─ select:contains → 文本包含
├─ 或按 Enter/逗号 直接创建
├─ 验证标签 chip 出现
└─ 去重(已存在则跳过)Examples
# 小红书话题
tag_add placeholder:"搜索话题" value:"Playwright" select:exact
tag_add placeholder:"搜索话题" value:"自动化测试" select:first
tag_add placeholder:"搜索话题" value:"小红书运营" select:contains
# 批量
foreach $t in ["Playwright", "自动化", "测试"]
tag_add placeholder:"搜索话题" value:$t select:first
endforeachOther tag commands
tag_remove placeholder:"搜索话题" value:"Playwright"
tag_clear placeholder:"搜索话题"
tag_list -> $tags # 提取当前所有标签
tag_select css:".tag-list" value:"已有标签" # 从已有列表中选择
assert_tag placeholder:"搜索话题" contains "Playwright"
assert_tag_count placeholder:"搜索话题" equals 3Trigger methods
# 通过 # 触发
tag_add css:".editor" value:"Playwright" trigger:"#"
# 通过按钮触发
tag_add css:".editor" value:"Playwright" trigger:click
# 直接输入 + 回车
tag_add css:".tag-input" value:"Playwright" mode:enter11. Form controls
Checkboxes & radios
check css:"#agree"
uncheck css:"#subscribe"
check css:".radio-option" [value="A"]
is_checked css:"#agree" -> $checkedDropdown selection
select css:"#city" value:"上海"
select css:"#city" label:"上海市"
select css:"#city" index:2
select_multi css:"#tags" values:"A","B","C"
deselect css:"#city" value:"上海"
select_native css:"#city" value:"上海" # 原生 select
select_custom css:".dropdown" value:"上海" # 自定义下拉Date · time · range
set_date css:"#date" value:"2026-09-29"
set_time css:"#time" value:"14:30"
set_range css:"#price" value:500
set_color css:"#color" value:"#ff0000"Sliders & ratings
set_slider css:".slider" value:75
set_rating css:".rating" value:512. Click & mouse
click <selector> [button=left|right|middle] [click_count=1] [force=false]
dblclick <selector>
right_click <selector>
hover <selector>
focus <selector>
blur <selector>
tap <selector> # 触摸
click_at x:100 y:200
click_position <selector> x:10 y:10
drag <from> to <to>
drag_to <selector> target:<selector>
scroll_to <selector>
scroll_by x:0 y:500
scroll_into_view <selector>
scroll_top / scroll_bottom13. Keyboard
keyboard_press "Enter"
keyboard_press "Control+A"
keyboard_type "Hello" delay:50
keyboard_down "Shift"
keyboard_up "Shift"
keyboard_insert_text "中文文本"14. Wait & sync
wait 1000
wait_until <selector> [state=visible|hidden|attached|detached] [timeout=30000]
wait_until url contains "/dashboard"
wait_until url equals "https://..."
wait_until text:"加载完成"
wait_until title contains "首页"
wait_until count css:".item" >= 10
wait_for_url "**/dashboard"
wait_for_response url contains "/api/user"
wait_for_request url contains "/api/submit"
wait_for_download to:"file.pdf"
wait_for_popup -> $new_page
wait_for_function "() => window.ready === true"
wait_for_load state:networkidle
wait_for_timeout 300015. Assertions
assert exists <selector>
assert not_exists <selector>
assert visible <selector>
assert hidden <selector>
assert enabled <selector>
assert disabled <selector>
assert text <selector> equals "预期"
assert text <selector> contains "包含"
assert text <selector> matches "/正则/"
assert value <selector> equals "值"
assert attr <selector> name:"href" contains "/home"
assert url contains "/dashboard"
assert title equals "首页"
assert count <selector> equals 5
assert count <selector> >= 3
assert checked css:"#agree"
assert uploaded <selector> count:3
assert tag <selector> contains "Playwright"
assert no_console_error
assert no_network_error# 失败策略
assert exists css:".ad" on_fail:continue
assert exists css:".required" on_fail:abort
assert exists css:".optional" on_fail:retry(3, 1000)16. Data extraction
get_text <selector> -> $var
get_text_all <selector> -> $list
get_attr <selector> name:"href" -> $url
get_value <selector> -> $val
get_html <selector> -> $html
get_inner_html <selector> -> $html
get_count <selector> -> $n
get_url -> $url
get_title -> $title
get_cookie name:"session" -> $cookie
get_local_storage key:"token" -> $token
get_all_text -> $page_text
get_screenshot <selector> to:"el.png"# 批量提取
get_all css:".item" -> $items {
text: "get_text(css:'.title')",
link: "get_attr(css:'a', 'href')",
price: "get_text(css:'.price')"
}17. Flow control
Conditionals
if $count > 3
echo "超过阈值"
elif $count == 0
echo "零"
else
echo "正常"
endifCounted loops
loop 5 times
click text:"下一页"
wait 1000
endloopIteration
foreach $item in ["a", "b", "c"]
echo $item
endforeach
foreach $file in glob("assets/*.jpg")
upload css:"input[type=file]" files:$file
endforeachConditional loops
while exists text:"加载更多"
click text:"加载更多"
wait 1000
endwhileLoop control
loop 10 times
if exists text:"结束"
break
endif
if not_exists text:"继续"
continue
endif
endloopReturn
func check()
if not_exists css:".ok"
return false
endif
return true
endfunc18. Functions & modules
Define & call
func login(user, pass)
goto $BASE + "/login"
input placeholder:"用户名" value:$user
input placeholder:"密码" value:$pass mode:keyboard
click role:button["登录"]
wait_until url contains "/home"
endfunc
call login("admin", env("APP_PASSWORD"))Default parameters
func publish(title, body, tags = [])
input placeholder:"标题" value:$title
input role:textbox[first] value:$body mode:richtext
foreach $t in $tags
tag_add placeholder:"搜索话题" value:$t select:first
endforeach
click text:"发布"
endfuncModule import
import "lib/xhs.bal"
import "lib/common.bal"
call xhs_publish("标题", "正文", ["标签1", "标签2"])Module export
# lib/xhs.bal
export func xhs_login(session_file)
...
endfunc
export func xhs_publish(title, body, tags)
...
endfunc19. Error handling
try / catch / finally
try
click text:"提交"
wait_until text:"成功"
catch e
echo "失败: " + $e.message
screenshot "error.png"
finally
echo "清理"
endtryon_error modifier
click text:"提交" on_error:retry(3, 1000)
click text:"可选广告" on_error:continue
upload css:"input[type=file]" files:"a.jpg" on_error:abortGlobal error policy
config on_error=abort # abort | continue | retry
config screenshot_on_error=true
config trace_on_error=true20. Dialogs · frames · windows
Native dialogs
dialog_accept
dialog_dismiss
dialog_handle accept
dialog_handle dismiss
dialog_prompt "输入内容"
on_dialog acceptiframe
frame css:"iframe#login"
input placeholder:"用户名" value:"admin"
click text:"登录"
frame_exit
frame_mainMultiple windows
new_window "https://example.com" -> $page
switch_window 1
switch_window title contains "新窗口"
close_window21. Network & storage
Intercept & mock
intercept url contains "/api/user" -> $response
mock url contains "/api/user" body:{"name": "test"} status:200
block url contains ".png"
unblock url contains ".png"
route url contains "/api/**" handler:"mock_handler"
unroute
wait_for_response url contains "/api/submit"Storage
storage_save "state.json"
storage_load "state.json"
cookie_set name:"token" value:"xxx" domain:".example.com"
cookie_get name:"token" -> $token
cookie_clear
local_storage_set key:"token" value:"xxx"
local_storage_get key:"token" -> $val22. Debugging
screenshot "step1.png"
screenshot <selector> to:"element.png"
echo "当前 URL: " + get_url()
log info "步骤完成"
pause # 打开 Inspector
trace_start "trace.zip"
trace_stop
dump html -> "page.html"
dump text -> "page.txt"
dump console -> "console.log"
dump network -> "network.har"
highlight <selector>23. Extensions
Register custom commands
# Python 侧
@executor.register("xhs_publish_note")
async def publish_note(ctx, title, body, tags):
...
# BAL 侧
xhs_publish_note title:"标题" body:"正文" tags:["a","b"]Register selector strategies
STRATEGIES["data"] = lambda p, v: p.locator(f'[data-{v}]')
click data:action="submit"Register upload strategies
UPLOAD_STRATEGIES["websocket"] = upload_via_ws
upload css:"#file" files:"a.jpg" mode:websocketSite config .page
site: xiaohongshu
base_url: "https://www.xiaohongshu.com"
selectors:
creator_button: 'text:"创作中心"'
title_input: 'placeholder:"添加标题"'
body_editor:
selector: 'role:textbox[first]'
input_mode: richtext
upload_input: 'css:input[type="file"]'
hashtag_entry: 'text:"#添加话题"'
hashtag_search: 'placeholder:"搜索话题"'
input_defaults:
mode: auto
verify: true
upload_defaults:
mode: auto
timeout: 180000
load_page "xiaohongshu.page"
goto creator
input $title_input value:"标题"24. Examples
XHS note publishing
# xhs_publish.bal
config input_mode=auto
config input_verify=true
config screenshot_on_error=true
load_page "xiaohongshu.page"
session load "account.json"
goto creator
# 上传图片(第一张为封面)
upload $upload_input \
files:"cover.jpg","detail1.png","detail2.png" \
mode:auto \
cover:"cover.jpg" \
verify:true
wait_upload_complete timeout:120000
# 标题(React 受控 input)
input $title_input value:"Playwright 自动化发布测试" mode:auto verify:true
# 正文(contenteditable 富文本)
input $body_editor value:"这是正文内容,测试富文本输入。" mode:richtext delay:30
# 标签
foreach $t in ["Playwright", "自动化测试", "小红书运营"]
tag_add $hashtag_search value:$t select:first verify:true
endforeach
assert_tag_count $hashtag_search equals 3
# 发布
click text:"发布"
wait_until url contains "/publish/success" timeout:60000
screenshot "publish_result.png"
session save "account.json"Video upload + attachments
config upload_timeout=600000
upload css:"input[type=file]" files:"video.mp4" mode:chunked chunk_size:5242880
wait_until text:"视频上传完成" timeout:600000
assert_uploaded css:".video-item" count:1
upload css:"input[type=file]" files:"cover.jpg" mode:auto
upload css:"input[type=file]" files:"doc.pdf","sheet.xlsx"Batch ops with error recovery
foreach $item in $items
try
input placeholder:"搜索" value:$item mode:type_slow delay:150
wait_until css:".result-item" state:visible
click css:".result-item" [first]
click text:"收藏"
wait_until text:"已收藏"
catch e
log error "处理失败: " + $item + " - " + $e.message
screenshot "error_" + $item + ".png"
continue
endtry
endforeach25. Appendix
Selector cheatsheet
text · role · label · placeholder · testid · title · alt · css · xpath · nth · ref
Input mode cheatsheet
fast · type · type_slow · keyboard · native · richtext · ime · auto
Upload mode cheatsheet
input · drag · paste · chunked · dir · auto
Error codes
| Code | Meaning |
|---|---|
| E_SELECTOR_NOT_FOUND | Selector has no match |
| E_TIMEOUT | Wait timeout |
| E_INPUT_FAILED | All input strategies failed |
| E_UPLOAD_FAILED | Upload failed |
| E_ASSERT_FAILED | Assertion failed |
| E_DIALOG_UNHANDLED | Dialog unhandled |
| E_FRAME_NOT_FOUND | Iframe not found |
| E_NETWORK_ERROR | Network error |
| E_PERMISSION_DENIED | Permission denied |
Coverage checklist
| Category | Coverage |
|---|---|
| Navigation | goto/back/forward/reload/multi-tab/multi-window |
| Elements | find/click/hover/drag/scroll/focus |
| Input | forms/rich text/read-only/IME/controlled/verify |
| Upload | image/video/attachment/dir/drag/paste/chunked |
| Tags | type/suggest/select/remove/dedup/batch |
| Forms | check/radio/select/date/time/range/slider |
| Wait | element/URL/text/network/download/popup/function |
| Assert | exists/visible/text/attr/count/upload/tag |
| Extract | text/attr/HTML/cookie/storage |
| Flow | if/loop/foreach/break/continue/return |
| Functions | define/call/defaults/import/export |
| Errors | try/catch/finally/retry/on_error |
| Dialogs | alert/confirm/prompt |
| Frames | iframe enter/exit |
| Network | intercept/mock/block/wait |
| Storage | session/cookie/localStorage |
| Debug | screenshot/log/pause/trace/dump |
Closing
- Never assume input: input mode:auto maximizes compatibility — every field has a fallback path.
- Never assume upload: upload mode:auto covers input, drag, paste, chunked and dir — five paths.
- Tags abstracted: tag_add wraps "type + suggest + select + dedup + verify" into one command.
Together with .page site configs and the command registry, this DSL adapts to any site and any control — covering the full web workflow from navigation to upload to extraction — without touching the language core.
