NAL — Nocarta Automation Language
NAL is Ruby. Every line of a script is either an ordinary Ruby statement or one of the commands listed on this page — the ones that open a page, fill a field, read a table, call an API or drive a Windows window. Anything else is refused before the run starts, with the line number and the form that exists.
That is the whole language, and it is worth saying precisely, because it is a promise rather than a family resemblance:
- What NAL accepts behaves exactly as Ruby does.
7 / 2is3.-7 % 2is1."abcdef".slice(1)is"b". If you know Ruby, nothing here will surprise you; if you do not, anything you read about Ruby is true here. - What NAL does not have, it refuses out loud. There are no classes, no
begin/rescue, nolambda, no libraries to require. You get an error naming the thing that exists instead, never a silent difference. - One deliberate exception, written out under “Maps” below:
:nameand"name"are the same key, because a separate Symbol type is the first thing that trips up someone who has never programmed.
Four things shaped it, in this order:
- Anyone who uses a browser and a spreadsheet can learn it. A script reads like plain instructions, with ordinary programming when you need it: variables, conditions, loops, functions. A mistake is reported with its line number before anything runs.
- The same words everywhere. Whether an automation drives a website, runs in your own browser tab, or drives a Windows application, the language is identical. Only the commands that talk to that kind of target change.
- An assistant can write it for you. Claude, ChatGPT or any other MCP client reads this same page, and can create, update and check your automations through Nocarta’s API. If you are that assistant: write Ruby.
- It is Ruby, so the names, the punctuation and the behaviour are all the ones you already know.
If you are looking for how to create an automation, bind a credential or dispatch a run, see Automation. This page covers only the language itself.
The three kinds of automation
All three are written in NAL. Every automation picks one of these when you create it. The kind decides where the script runs and which commands it may use; the language is the same in each.
| Kind | Runs on | NAL, plus |
|---|---|---|
| Desktop | The Nocarta desktop app, driving its own browser | The page commands, js / pw |
| Browser | Your own browser tab, no desktop app needed | Nothing extra: the core language, http and api |
| Windows | The Nocarta desktop app, driving a Windows application | The Windows commands |
The core language below is NAL itself and works in all three, and so do
http requests and api calls. Everything else is listed under the kind it
belongs to. If you use a command your kind does not have, the editor tells you
when you save — it names the command and the kind that does have it, so nothing
surprising happens mid-run.
Core language (every kind of automation)
Comments
A line that starts with # is a comment. After a command, # starts a
comment when it has a space before and after it. # is the only comment marker.
# find the invoice table first
click ".invoices" # then open it
Variables
name = "Maria Silva"
count = 0
count += 1 # also -= *= /=
rows[0] = "first"
totals["march"] = 1200
Some words belong to the language and cannot be used as names: every
statement word (log, ask, validate, output, stuck, finish, if,
for, def, …) and the first word of every command (click, fill, type,
read, list, table, page, launch, …), in any capitalisation. Writing
type = 1 or for click in rows stops with an error that names the word and
suggests another name, such as type_value. The words are still fine after a
dot or as a key: row.type and {page: 2} work.
Put a value inside text with #{...}, in double quotes:
log "found #{rows.length} rows for #{name}"
Single quotes keep the text exactly as written: '#{name}' is those seven
characters, and so is '{{PORTAL.PASSWORD}}' — nothing inside single quotes
is ever replaced. To keep a literal #{ or {{ inside double quotes, write
\#{ or \{{.
The last value produced — by a command, an expression or an assignment — is
always _, an ordinary variable:
read table "#grid"
log "rows: #{_.length}"
rows = _
Values
Text, whole numbers, decimals, true, false, nil, lists and maps.
items = ["a", "b", "c"]
person = {name: "Maria", age: 41}
range = 1..10
exact = 1...10 # 1 to 9 — the end is left out
false and nil are false; everything else is true, including 0 and
"". A number and a piece of text never mix silently: "5" + 1 is an error,
"5".to_i + 1 is 6.
Whole numbers and decimals are different kinds of number, as they are in
Ruby. 7 / 2 is 3, because dividing two whole numbers gives a whole number;
7 / 2.0 and 7.fdiv(2) are 3.5. A decimal writes itself with its point, so
5.0 reads as "5.0" — call .to_i or .round before putting one in a form
field.
Maps: {name: "Maria"}, {"name" => "Maria"} and {"name": "Maria"} all
build the same thing, and person[:name], person["name"] and person.name
all read it. This is the one place NAL parts from Ruby on purpose: a map key
is always text, so :name and "name" are the same key. (In Ruby they are
not.) Two consequences worth knowing: a key that is also a method name, like
length, is reachable as person["length"] rather than person.length; and
keys that read as whole numbers come back before the others when you list them.
Conditions
if count > 10
log "plenty"
elsif count > 0
log "a few"
else
log "none"
end
unless paid
log "open invoice"
end
Compare with == != < <= > >= <=>. Combine with &&, ||, ! or
their word forms and, or, not — the words bind more loosely, exactly as
in Ruby:
if total > 0 && !paid
log "open invoice"
end
x || "N/A" gives x, or "N/A" when x is nil or false.
A condition can sit at the end of the line it governs, and if itself has a
value:
log "open invoice" if total > 0
status = if paid
"closed"
else
"open"
end
case picks a branch by value, by range, or by pattern:
case amount
when 0
log "nothing to pay"
when 1..99
log "small"
when /^EUR/
log "already in euros"
else
log "large"
end
Loops
rows.each do |row|
break if row == "stop"
log row
end
rows.each_with_index do |row, i| # with the position
log "#{i}: #{row}"
end
5.times { |n| log n }
for n in 1..5
log n
end
while count < 10
count += 1
end
until done
done = check_something
end
break leaves the loop, next skips to the following item; both take a value
(break row) which becomes the loop’s answer.
A do |row| … end block is how Ruby loops, and it is what an assistant will
write. for exists too and reads more plainly; the only difference is that
for leaves its variable behind after the loop, as Ruby’s does.
Blocks are only ever attached to a method call. You cannot store one in a
variable, so lambda, -> and yield are not part of NAL.
Functions
def full_name(first, last)
"#{first} #{last}"
end
log full_name("Maria", "Silva")
A function gives back the value of its last line; return leaves early. A
function cannot take a block — pass it the values it needs.
Saying things
Two separate channels, and the difference matters:
log "starting the invoice run" # for a person — chat panel and run log
output invoices: rows, total: amount # for the next step — machine-readable
output takes key: value pairs, keys in lower_snake_case. Use it more
than once and the pairs merge; the last value written for a key wins. To hand
instances to the next step of a recipe, emit them as instance_ids.
ask puts a question to the assistant and gives you back its answer:
ask "which of these looks like the invoice total? #{cells}"
answer = _
Add , model: "…" to say which model should answer — the name of one of your
own models, or of one of ours.
Deciding
ask gives you words back. decide gives you an answer with a number
attached, from a model built for judging rather than for writing:
d = decide "Is this a refund request?" about notes
if d.answer
log "refund, #{(d.probability * 100).round}% sure"
end
team = decide "Which team should handle this?" about notes among ["billing", "support"]
level = decide "How urgent is it?" about notes levels ["calm", "normal", "urgent"]
Write the question, then about what it is about — text, a map or a list.
With nothing else, the answer is yes or no: d.answer is true or false,
and d.probability is how sure the model is. among gives it a list of names
to pick from; levels gives it an ordered scale to place things on. Those two
answer with d.answer and d.confidence.
Two things can follow, after a comma:
d = decide "Is this a refund?" about notes, threshold: 0.7, model: "Airwalk"
threshold: is where yes begins — 0.5 unless you say otherwise, so a
probability of 0.6 is a yes by default and a no at 0.7. model: names which
of your models answers.
Every answer also carries d.answered_by, which tells you which model actually
answered — so a script can check it was the one you asked for.
A decision model is one of your own, set up in Settings → Preferences → LLM Configuration. It always runs on your own machine through Nocarta Desktop: in a browser automation the question is sent to your desktop and the answer comes back, so your desktop has to be running.
Asking the person watching
code = im_stuck "Type the code from your phone", expect_input: true
ok = im_stuck "Continue with the import?"
im_stuck pauses the run and waits for the person. With expect_input: true
you get what they typed; without it, true when they confirm. Cancelling stops
the run, unless you add fail_on_cancel: false, which gives false instead.
It works where a person is watching: the assistant chat, the debugger, or a
recipe step someone started by hand. A batch or an unattended run stops instead.
Checking as you go
validate rows.length > 0
If the check comes out false the run stops there and says so. It never quietly carries on — which is the whole point of writing the check.
Finishing, and getting stuck
checkpoint "logged-in" # a named point you can resume from
finish "nothing to invoice today" # end the run early, successfully
stuck "the portal is down for maintenance", category: "external_outage"
on fail message "Could not reach the invoice list"
on stuck restart from checkpoint max 2 with fresh browser
checkpointgoes at the top level of the script, or inside anif. Not inside a loop or a function, and each name is used once.finishends the run as a success.stuckends it as needing help, and the message is what the person reading the run will see.on stuck restartis written once, at the top level.maxis 1 to 5, and you restartfrom start,from checkpointorfrom command.with fresh browsercombines with the first two, in Desktop automations.
Working with values
Methods chain: rows.first.upcase. To work on what a command gives back,
capture it first:
cells = read row 0
log cells.join(", ")
The methods are Ruby’s, with Ruby’s names and Ruby’s behaviour. Every Ruby method on text, whole numbers, decimals, lists, maps and ranges that does not take a block is here, and so are the block ones listed below. Anything else is refused by name.
Text — length size empty? upcase downcase capitalize swapcase
strip lstrip rstrip chomp chop reverse chars lines to_s
to_i to_f split slice sub gsub scan match match? index
rindex include? start_with? end_with? ljust rjust center tr
delete squeeze count partition rpartition succ ord %
each_char each_line
Lists — length size empty? first last reverse flatten
compact uniq sort slice include? index rindex concat join
push pop shift unshift insert delete delete_at min max sum
take drop rotate zip product transpose tally fetch dig
values_at to_a to_h count any? all? none?
— and with a block: each each_with_index each_with_object map select
reject find find_index filter_map flat_map reduce sort_by
min_by max_by group_by partition take_while drop_while each_slice
each_cons uniq sum count any? all? none?
Maps — keys values length size empty? include? key?
has_key? value? key fetch dig merge delete invert to_a
to_h values_at — and with a block: each map select reject find
transform_values transform_keys sort_by sum count any? all?
Numbers — to_s to_i to_f abs ceil floor round truncate
fdiv divmod even? odd? zero? positive? negative? succ pred
gcd lcm digits clamp between? chr — and with a block: times
upto downto step
Ranges — to_a size count first last min max sum include?
cover? exclude_end? — and every list method that takes a block.
The behaviour is Ruby’s, which means:
gsub("a", "b")changes everya;subchanges only the first. Both take a pattern too:gsub(/\d+/, "#").splitwith nothing splits on spaces and drops empty ends:"a b c".splitgives["a", "b", "c"].split(" ", 2)gives at most two parts:"ROSSI MARIO LUIGI".split(" ", 2)is["ROSSI", "MARIO LUIGI"].slice(1)gives ONE item;slice(1, 3)gives three from position one;slice(1..3)does the same."abcdef"[1, 3]is another way to write it.sortneeds a list it can order — one that mixes numbers and text is an error, because neither order is right.compactremovesnilonly.""and0are values and stay.joinwith nothing puts the items straight together;join(", ")separates.concat,push,popand their kind change the list in place, as Ruby’s do.+gives a new one.to_ion text that is not a number is an error, not0. This is the one place NAL refuses something Ruby answers, and it is deliberate: a field reading “N/A” must never quietly count as zero in a total.- Two digits is
rjust(2, "0"):"7".rjust(2, "0")is"07".
Regular expressions are Ruby’s, written between slashes:
if code.match?(/^INV-\d{4}$/)
number = code.match(/\d+/)[0]
end
clean = name.gsub(/[^a-zA-Z0-9 ]/, "")
i ignores case, m lets . match a line break, x lets you space the
pattern out.
Built-in functions
today() # "2026-09-15"
today("dd/MM/yyyy")
now() # "2026-09-15 14:30:00"
weekday(2026, 9, 15) # "TUE" — add a language: weekday(2026, 9, 15, "it")
days_in(2026, 9) # 30
format_date(2026, 9, 15, "EEE dd MMM")
url_encode("Ada & co") # "Ada%20%26%20co" — safe inside a URL
exists(".invoice-table") # true when something on the page matches
Date tokens: yyyy MM dd HH hh mm ss tt EEE.
exists needs a page to look at, so it is available to Desktop
automations.
The data a run is given
A run over a form instance sees that instance as instance; a step in a recipe
sees what the previous step emitted as input. Both are ordinary maps:
fill "#name" with "#{instance.fields.full_name}"
fill "#code" with instance.fields["code_" + day]
open "https://portal.example.com/invoice/#{input.invoice_id}"
Reading a field the run was not given is an error, never a blank — a blank
would let a form be submitted empty while the run reports success. To ask
whether it is there, use || or compare with nil:
phone = instance.fields.phone || "N/A"
Every field of the template is there, filled or not. A field nobody wrote
anything in arrives as "", so you can read it, compare it and fill with it
like any other. What stops the run is a name that is not on the template —
instance.fields.notse for a field called notes is an error naming it, which
is how a typo is caught instead of quietly filling a form with nothing.
instance.fields walks as [name, value] pairs — instance.fields.each do |name, value| — and so do instance.metadata,
instance.instance_id and instance.template_id are also there.
Calling other people’s APIs
http makes a request, in every kind of automation:
r = http get "https://api.example.com/items/#{id}"
r = http post "https://api.example.com/cards?list=#{url_encode(list_id)}", {
headers: {"X-Request-Source": "nocarta", Authorization: "Bearer {{TRELLO.VALUE}}"},
body: {name: title, desc: "Card for #{customer}"}
}
r = http put "https://api.example.com/items/#{id}", {body: {name: title}}
r = http patch "https://api.example.com/items/#{id}", {headers: {"If-Match": etag}, body: {name: title}}
r = http delete "https://api.example.com/items/#{id}"
A request is always one line: the method, the address, and — when you need to
say more — one {…} after a comma holding headers: and body:. The {…}
may be spread over several lines, as here. There is nothing else to learn: a
token is an Authorization header like any other, and a query string is part
of the address, written there with url_encode(value) around anything that
could contain a space or an &.
Methods: get post put patch delete.
You get back {status, status_text, headers, body, json}. A 404 or a 500 is
a result you inspect, not a failure — only a request that could not be made
at all stops the run:
r = http get "https://api.example.com/invoices"
if r.status == 200
log "got #{r.json.length} invoices"
else
stuck "The invoice API answered #{r.status}"
end
The body’s kind decides how it is sent: a map or a list goes as JSON, text
goes exactly as you typed it. Write body: {name: title}, never
body: "{\"name\": \"#{title}\"}" — the map is turned into correct JSON for
you, whatever the values contain. A get request has no body.
Requests go only to the hosts listed on the automation’s Settings tab. An empty list reaches nothing.
Your Nocarta data
api reads and writes your own Nocarta data from inside a script, in every
kind of automation. You give it a path, not a full address — the script works
the same wherever it runs, and you never paste a token into it:
instances = api get "/form_instances?form_template_id=#{template_id}"
result = api post "/form_instances/bulk_create", {
body: {form_template_id: template_id, instances: rows}
}
api put "/form_instances/#{id}", {body: {name: "Checked"}}
api patch "/form_instances/#{id}", {body: {name: "Checked"}}
api delete "/form_instances/#{id}"
Same request hash and same result shape as http. The script runs as you, with your
own permissions. api get "/documentation" lists everything it can reach.
Credentials
Never type a password into a script. Bind the credential to the automation and refer to it by name, in capitals:
fill "#password" with "{{PORTAL.PASSWORD}}"
{{...}} in capitals is the only use of double braces: it means a
credential. The placeholder is what is stored, and what appears in logs. The
real value is put in at the last moment, on the machine running the automation.
The script never contains it and never prints it. See Secrets.
A placeholder is filled in only where you wrote it inside double quotes (or in
js/pw code). Inside single quotes it is plain text, like everything else
there: '{{PORTAL.PASSWORD}}' reaches the page as those braces. Text that
comes from a page or from an instance field is never filled in either, even if
it happens to look like a placeholder.
Desktop automations: page commands
These drive the browser inside the Nocarta desktop app.
A command can stand alone, or be the whole right-hand side of an assignment:
open "https://example.com/invoices"
rows = read table
Moving around
open "https://example.com"
back
forward
refresh
screenshot
close browser
browser status
Doing things on the page
click "#submit" # a CSS selector
click text "Log in" # whatever carries that text
click "#row-9" holding "Shift" # click with keys held down
fill "#email" with "[email protected]"
fill "Maria Silva" into "the customer name box" # described in words
type "some text" # keystrokes, wherever the focus is
select "Italy" from "#country"
press "Enter" # a key name
press "Control+A" in "#notes"
press "Cancel" # not a key name: clicks that button
search and click "the blue download button"
fill … with names the field with a selector; fill … into describes it in
words and lets the assistant find it. type sends keystrokes to whatever
already has the focus — to put a value in a named field, use fill.
Key names are the usual ones: Tab Enter Escape Backspace, F1–F24,
the arrows, Home End PageUp PageDown, and chords like Control+A.
click … holding holds keys down for the length of the click — Shift to
extend a selection to the row you click, Control to add one row without
losing the rest. Name several with +, the same way a chord is written:
click "#row-3"
click "#row-9" holding "Shift" # rows 3 to 9
click "#row-12" holding "Control+Shift"
You can hold Shift Control Alt Meta, or ControlOrMeta for the one
that means “the usual shortcut key” on whichever system the automation runs on.
Holding keys works with a selector only — click text "…" finds its element by
reading the page, and that kind of click cannot carry held keys.
Looking at the page
links = list links
links = list links "invoice" # only those matching
matches = find by text "Total"
box = find input near "Customer name"
thing = find "the download button" # described in words
items = find on selected
text = read focus # what is in the focused field
layout = show elements
sel = get selector
find by text, find … near and find give back nil when nothing matches,
so you can test them:
button = find "the export button"
if button
click button
end
find input near "…" accepts input button select textarea link a
checkbox radio field element.
Tables
rows = read table
rows = read table ".invoices"
row = read row 2
cell = read table cell row 2 col 3
col = find column "Amount" in row 0
fill table row 2 col 3 with "180,00"
switch to table ".invoices"
exit table
Frames, tabs and windows
list iframes
list frames
switch to iframe "#payment"
switch to frame "content"
exit iframe
list pages
switch to page "last"
switch to page "invoices"
switch to page 2
Handy
sleep 2 # seconds
highlight ".total" red # red blue green black white orange
clear highlights
When the commands are not enough
js document.querySelectorAll(".row").length
js
const rows = document.querySelectorAll(".row")
return rows.length
end
js with customer, year
return findInvoice(customer, year)
end
pw
await page.getByRole("button", {name: "Export"}).click()
end
js runs JavaScript in the page, pw runs a Playwright instruction. The code
inside is passed through exactly as written — nothing is substituted in
it, so #{...} does not work there, and # is not a comment there either.
To get a script value in, list it after with and use it as an ordinary
variable. A credential placeholder {{ALIAS.FIELD}} is the one thing that
does get filled in.
Browser automations
These run inside your own browser tab, and need no desktop app — the only kind
that works on a phone. They use the core language, http and api, exactly as
described above. There are no page commands (there is no automated browser), no
js / pw, and no exists.
r = http get "https://api.example.com/invoices"
validate r.status == 200
api post "/form_instances/bulk_create", {
body: {form_template_id: template_id, instances: r.json.items}
}
Windows automations
These drive a native Windows application through the desktop app. The control names and key notation are those of the Windows UI Automation library the desktop app uses.
Every command has one written form, and the editor checks it when you save: a
missing word is an error that shows the form. A control is written as its
kind, bare, then its name in quotes, then the window it lives in:
button "Save" in "Invoices". Kinds: button, edit, checkbox,
combobox, menu, menu item, tree, tree item, datagrid, tab,
link, radio.
Commands that give you something back. Each names the window it reads from:
title = read title of "Invoices"
text = read text from "Invoices"
value = read value of edit "Name" in "Invoices"
grid = read grid "Invoices"
grid = read grid datagrid in "Invoices"
cell = read grid cell row 0 col 1 "Invoices"
fields = read fields in "Invoices"
ctrls = read controls button in "Invoices"
tree = read tree of "Invoices"
dialog = read dialog
list windows
read controls lists every control of one kind in a window, and read tree
gives you the whole window as the application lays it out: both are how you
find out what things are called before you write the commands that act on
them. read grid cell counts rows and columns from 0.
What you get back: read title, read text, read value of and
read grid cell give text. read grid and read tree give a map you look
inside (grid.headers, grid.cells[0][1], tree.tree); read dialog gives a
map when a dialog is open and false when none is; read fields,
read controls and list windows give a list.
Commands that act:
launch "C:\\Program Files\\App\\app.exe"
launch "notepad.exe" "C:\\notes.txt"
focus window "Invoices"
click button "Save" in "Invoices"
set value of edit "Name" in "Invoices" to "Maria Silva"
type "some text" into "Invoices"
press "Enter" in "Invoices"
select combobox "Country" in "Invoices"
toggle checkbox "Active" in "Invoices"
expand tree item "Edit" in "Invoices"
collapse tree item "Edit" in "Invoices"
invoke button "Refresh" in "Invoices"
focus edit "Name" in "Invoices"
click dialog button "OK"
set clipboard to "text"
wait 500
sleep 2
close window "Invoices"
close app "app.exe"
wait and sleep are not the same unit. wait belongs to the Windows
vocabulary and counts milliseconds, like the rest of it — wait 500 is half
a second. sleep is the core command and counts seconds everywhere in the
language, here included. Writing wait 2 pauses for two milliseconds, which
looks like no pause at all.
These are all the Windows commands there are. Anything else is not a command: the editor refuses it when you save, naming the line.
When something goes wrong
A command that finds nothing fails the step — click "#missing" stops the
run rather than passing silently. That is deliberate: a script that quietly
skips its own work is worse than one that stops. When an element really is
optional, say so:
if exists(".cookie-banner")
click text "Accept"
end
Ruby that NAL does not have
NAL is a subset of Ruby. These are refused by name, with the thing that exists in their place — never silently.
| Ruby | Why not, and what to write |
|---|---|
begin / rescue / ensure / raise / retry |
A run does not catch its own failures. Stop with stuck "…", assert with validate, set the message a person sees with on fail message, and retry with on stuck restart. |
class, module, attr_accessor, self |
A script is statements and defs. |
lambda, ->, proc, yield, &blk |
A block is only ever attached to a method call; it is never a value. Name reusable code with def. |
require |
There are no libraries to load. |
rand, sample, shuffle |
A run has to be repeatable. |
freeze, dup, clone, object_id, class, is_a? |
There is no object identity to work with. |
Date, Time, File, IO, gets |
Use today(), now(), format_date(), and im_stuck to ask a person. |
[1, 2].map with no block, each_slice(2).to_a |
There is no Enumerator: every one of these methods needs its block. |
"abc".to_i, nil.to_i |
Ruby answers 0. NAL stops instead, so a field reading “N/A” never counts as zero. |
"a" << "b" |
Text never changes in place. Write a += "b". |
2 ** -1 |
That is a fraction in Ruby, and NAL has no Rational. Write 1.0 / (2 ** 1). |
x = a and b |
Ruby reads this as (x = a) and b, which is almost never what someone means. Write x = a && b. |
Older NAL spellings
The language used to have its own names for some of these. Each is refused with a message naming the Ruby form.
| Instead of | Write |
|---|---|
set x = 5, preset, runtime, init |
x = 5 |
// a comment |
# a comment |
each x in items, repeat 5 |
for x in items, 5.times do |n| |
x ?? "N/A" |
x || "N/A" |
else if, elseif |
elsif |
"{{name}}", "{{instance.fields.x}}" |
"#{name}", "#{instance.fields.x}" — double braces are for credentials only |
cells.includes("a") |
cells.include?("a") |
s.starts_with("a"), s.ends_with("z") |
s.start_with?("a"), s.end_with?("z") |
s.replace(a, b), s.replace_first(a, b) |
s.gsub(a, b), s.sub(a, b) |
s.pad_left(3, "0"), s.pad_right(3), s.pad_zero |
s.rjust(3, "0"), s.ljust(3), s.rjust(2, "0") |
s.remove_special |
s.gsub(/[^a-zA-Z0-9 ]/, "") |
push items, "a" |
items.push("a") |
name result rows |
rows = read table |
echo "hi", report "hi" |
log "hi" (or Ruby’s puts) |
click if present "#x" |
if exists("#x") … end |
type "Maria" into "#name" |
fill "#name" with "Maria" |
read active, click by text "Go" |
read focus, click text "Go" |
output rows |
output rows: rows |
press Enter |
press "Enter" — a bare word is a variable |
Examples
Walk a site and report what is there
open "https://example.com"
links = list links
log "found #{links.length} links"
screenshot
output link_count: links.length
Fill a form and check it went through
open "https://example.com/contact"
fill "#name" with "Maria Silva"
fill "#email" with "[email protected]"
click text "Send"
if exists(".confirmation")
log "submitted"
else
stuck "No confirmation appeared after sending the form"
end
Log in, then read a table
on stuck restart from checkpoint max 2 with fresh browser
open "https://portal.example.com/login"
fill "#user" with "{{PORTAL.USERNAME}}"
fill "#password" with "{{PORTAL.PASSWORD}}"
click text "Sign in"
validate exists(".dashboard")
checkpoint "logged-in"
open "https://portal.example.com/invoices"
rows = read table
rows.each do |row|
log row.join(" | ")
end
output invoices: rows
Fill a portal from an instance, one run per instance
open "https://portal.example.com/customers/new"
fill "#name" with "#{instance.fields.full_name}"
fill "#email" with instance.fields.email
fill "#phone" with instance.fields.phone || ""
click text "Save"
validate exists(".saved")
Read your own Nocarta data and post results back
template_id = "your-template-id"
existing = api get "/form_instances?form_template_id=#{template_id}"
log "#{existing.json.length} entries already"
open "https://portal.example.com/invoices"
rows = read table
api post "/form_instances/bulk_create", {
body: {form_template_id: template_id, instances: rows}
}
finish "imported #{rows.length} invoices"
Need More Help?