Directive & Remote Computing

DIRECTIVE sends commands to a registered remote machine. The oql-client agent is installed on any machine — it connects outbound to ql.ocalt.com, receives commands, and executes them natively. No inbound ports. No firewall rules required. The agent does not run an OcaltQL runtime — it executes shell commands, file transfers, and system operations directly. All OcaltQL logic stays on the server side.

Inline Operations

Every DIRECTIVE operation is a single inline statement — there is no OPEN ... CLOSE block form. The agent executes shell commands, file transfers, and system operations directly; all OcaltQL logic (conditionals, loops, variable handling) stays on the server. EXEC can run any shell command on the target machine, which means any operation the machine supports is reachable through it.

Machine Slots

Every machine must claim a slot before it can be addressed. DIRECTIVE REGISTER claims one for a machine id of your choosing — that id is what you use in every later DIRECTIVE call. Calling DIRECTIVE against an id that was never registered is a fatal error.

Every account holds 4,096 machine slots - the same on every plan. A slot is taken when a machine registers and released when it is removed.

A released slot is held for 24 hours. Within that window the machine id cannot be re-registered, which keeps an id stable for anything already addressing it. This is a mechanism, not a plan limit - it applies identically on every plan.

A claimed slot is held for 24 hours. Within that window it cannot be released or reassigned — this keeps a machine id stable for anything already addressing it. After 24 hours, DIRECTIVE UNREGISTER frees the slot for another machine.

Registering never displaces a machine you already have. If every slot is in use, DIRECTIVE REGISTER fails with a fatal error telling you either to release a slot, or how many hours remain until the oldest one can be released. Re-registering an id you already hold is harmless and does not consume a second slot.
Claim a Slot, Then Use the Machine
DIRECTIVE REGISTER "home-nas" SET ?reg
AFTER EMIT ?reg("slots")
AFTER EMIT ?reg("used")
AFTER DIRECTIVE "home-nas" EXEC "uptime" SET ?r
AFTER EMIT ?r("stdout")
Release a Slot After 24 Hours
DIRECTIVE UNREGISTER "old-laptop" SET ?rel
AFTER EMIT ?rel
AFTER DIRECTIVE REGISTER "new-laptop" SET ?reg
AFTER EMIT ?reg
Fatal Errors
DIRECTIVE "never-registered" EXEC "echo hi"
(* Fatal: unregistered machine id 'never-registered' — claim a slot with DIRECTIVE REGISTER first *)
DIRECTIVE REGISTER "one-too-many"
(* Fatal: 'one-too-many' was registered less than 24 hours ago and
   cannot be re-registered yet — it unlocks in 19 hour(s) *)

Agent Management

List and Check Agents
DIRECTIVE LIST SET ?agents
AFTER EMIT ?agents("agents")
AFTER EMIT ?agents("used")
AFTER EMIT ?agents("slots")
AFTER DIRECTIVE STATUS "my-home-server" SET ?status
AFTER EMIT ?status("online")
AFTER EMIT ?status("last_seen")
AFTER EMIT ?status("seconds_since_seen")

An agent checks in with the hub on its POLL interval, and every check-in is recorded. DIRECTIVE STATUS "id" returns online, which is true when the machine checked in within the last 15 seconds; last_seen, the moment of its last check-in as "YYYY-MM-DD HH:MM:SS" in the hub's time; and seconds_since_seen, how long ago that was. A machine that is registered but has never checked in reports null for both. The answer also carries the machine’s approximate location, lat and lon, described under The Machine’s Location. Each entry of ?agents("agents") from DIRECTIVE LIST carries machine_id and the same fields. A machine polling every few minutes shows online as false between check-ins; seconds_since_seen against its interval tells whether it is merely between check-ins or gone.

POLL - How Often an Agent Checks In

An agent polls for work on an interval. POLL sets it, per machine, and takes effect on that machine’s next check-in.

Setting the Interval
DIRECTIVE "personal-server" POLL 1 SECONDS
AFTER DIRECTIVE "home-nas" POLL 5 MINUTES
AFTER DIRECTIVE "backup-box" POLL 60 MINUTES
The default is 1 second. The minimum is 1 SECOND and the maximum is 60 MINUTES - anything outside that range is an error. A shorter interval means a command reaches the machine sooner; a longer one is kinder to a metered or battery-powered connection.

EXEC and KILL — Remote Shell

Shell commands run directly on the target machine. BACKGROUND makes the call non-blocking, returning a process handle immediately instead of waiting for completion.

Blocking Command
DIRECTIVE "personal-server" EXEC "apt-get update && apt-get install -y ffmpeg" SET ?r
AFTER EMIT ?r("stdout")
Background Process and Kill
DIRECTIVE "personal-server" EXEC "python3 /home/worker.py" BACKGROUND SET ?pid
AFTER SLEEP 5 SECONDS
AFTER DIRECTIVE "personal-server" KILL ?pid

DOWNLOAD and UPLOAD

Transfers stream directly between the machine and your namespace, so file size is bounded by your storage quota rather than by memory. DOWNLOAD pulls a file from the machine into the namespace; UPLOAD pushes one the other way. Namespace paths are absolute — /root/... or /mounted/....

DOWNLOAD and UPLOAD never hold the script. Each returns at once with an object carrying the command id, the transfer's tid and its state, and the file moves in the background while the script carries on. At that moment the file has usually not arrived yet. To use the file in the same script, wait for it: ask DIRECTIVE STATUS about the transfer until its state is done, or failed.

Download, Then Wait for It to Land
DIRECTIVE "home-nas" DOWNLOAD "/srv/reports/latest.csv" TO "/root/latest.csv" SET ?d
AFTER DIRECTIVE STATUS ?d SET ?p
AFTER WHILE ?p("state") IS NOT EQUAL TO "done" AND ?p("state") IS NOT EQUAL TO "failed"
OPEN
  SLEEP 1 SECONDS
  AFTER DIRECTIVE STATUS ?d SET ?p
CLOSE
AFTER EMIT ?p("state")
AFTER FILE READ "/root/latest.csv" SET ?csv
(* First path is on the machine, second is in your namespace.
   FILE READ runs only once the transfer reports done. *)
Upload
DIRECTIVE "personal-server" UPLOAD "/root/config.json" TO "/etc/app/config.json" SET ?upl
Start a Transfer, Keep Working, Check Later
DIRECTIVE "personal-server" DOWNLOAD "/var/big.bin" TO "/root/big.bin" SET ?job
AFTER EMIT "transfer started, script keeps running"
AFTER STRING ?job("tid") SET CONSISTENT "big_download"
(* A later script reads the tid back and asks after the same transfer:
   DIRECTIVE STATUS ?tid SET ?p, then ?p("state"), ?p("percent") *)

PEERS and NETWORK — Machine-to-Machine Transfer

DIRECTIVE PEERS groups several registered machines into one network value. DIRECTIVE NETWORK ... TRANSFER then moves a file directly between two of them - the bytes travel machine to machine, not up through the server and back down. Each endpoint is written as a machine id, then ://, then a path on that machine.

A device joins a peer network once. Naming the same machine twice in DIRECTIVE PEERS is refused with an error, and so is a TRANSFER whose two ends are the same machine or a SHARE that lists a machine twice. Every transfer and share needs two different machines.
How two machines reach each other. The hub only coordinates: it tells both machines to connect and passes each one the other’s address. They first try to reach each other directly. When a network blocks that, as many mobile and strict office networks do, the connection falls back to a relay on Ocalt’s TURN server, the same mechanism video calls use. The hub never carries the data. A transfer reports buffering while the two machines connect, in progress while the file moves, done when it has landed, and failed if it could not complete; an interrupted transfer resumes from where it stopped.
Progress, from any script. TRANSFER returns at once, with a tid in its object; it never holds the script while the file moves. DIRECTIVE STATUS on that object, or on the tid alone, returns state, bytes moved so far, total (the file’s size) and percent. Keep the tid in CONSISTENT or PERSISTENT and a later script can ask after the same transfer.
DOWNLOAD and UPLOAD report progress too. Moving a file between a machine and your namespace is a transfer like any other. The object DOWNLOAD or UPLOAD returns carries a tid beside its command id, and DIRECTIVE STATUS on that object, or on the tid alone, returns the same state, bytes, total and percent. None of these calls hold the script: the file moves in the background while the script carries on. Any other directive call can be made the same way with SET PROMISE and collected later with WAIT FOR.
A large DOWNLOAD, watched while it moves
DIRECTIVE "laptop" DOWNLOAD "D:/footage/day1.mp4" TO "/root/footage/day1.mp4" SET ?d
AFTER SLEEP 5 SECONDS
AFTER DIRECTIVE STATUS ?d SET ?p
AFTER EMIT ?p("state") & ": " & ?p("percent") & "% of " & ?p("total") & " bytes"
Any directive call, without holding the script
DIRECTIVE "personal-server" EXEC "./nightly-report.sh" SET PROMISE ?job
AFTER EMIT "report started"
AFTER WAIT FOR ?job SET ?r
AFTER EMIT ?r("stdout")
READ and WRITE move bytes, exactly. A file of any format in any encoding (an image, an archive, UTF-16 text, anything with a zero byte in it) reads back byte for byte, and WRITE writes exactly what it is given: a string as its UTF-8 bytes, a READ result or any bytes value as itself. ?r("data") is a string when the file is valid UTF-8 and bytes otherwise; ?r("bytes") is the size either way. So DIRECTIVE "a" READ "x" SET ?r then DIRECTIVE "b" WRITE "y" CONTENT ?r("data") copies any file faithfully.
Start a transfer in one script…
DIRECTIVE PEERS "personal-server" AND "laptop" SET ?net
AFTER DIRECTIVE NETWORK ?net TRANSFER FROM "personal-server:///backups/site.tar" TO "laptop://D:/backups/site.tar" SET ?t
AFTER STRING ?t("tid") SET CONSISTENT "backup_transfer"
…and check it from another
CONSISTENT "backup_transfer" SET ?tid
AFTER DIRECTIVE STATUS ?tid SET ?p
AFTER EMIT ?p("state") & ": " & ?p("bytes") & " of " & ?p("total") & " bytes, " & ?p("percent") & "%"
Build a Peer Network
DIRECTIVE PEERS "deviceid1" AND "deviceid2" AND "deviceid3" SET ?network
AFTER EMIT ?network(0)("id")
AFTER COUNT ?network SET ?n
AFTER EMIT ?n & " peers"
Transfer Between Two Peers, Then Poll It
DIRECTIVE PEERS "deviceid1" AND "deviceid2" AND "deviceid3" SET ?network
AFTER DIRECTIVE NETWORK ?network TRANSFER FROM ?network(0)('id') & "://c:/downloads/file.zip" TO ?network(1)('id') & "://var/www/folder/destfile.zip" SET ?statusanchor
AFTER DIRECTIVE STATUS ?statusanchor SET ?progress
AFTER IF ?progress("state") IS IDENTICAL TO "buffering"
OPEN
  SLEEP 5 SECONDS
CLOSE
OR IF ?progress("state") IS IDENTICAL TO "done"
OPEN
  EMIT "peer transfer done"
CLOSE
OR
OPEN
  EMIT "still in progress check back later"
CLOSE
Share a Peer’s Port Across the Network
DIRECTIVE PEERS "deviceid1" AND "deviceid2" AND "deviceid3" SET ?network
AFTER DIRECTIVE NETWORK ?network SHARE ?network(0)('id') PORT 80 AS 8080 SET ?status
(* Any machine in the network can now reach node 0's port 80
   at its own localhost:8080 *)
SHARE is peer-to-peer, not a public tunnel. PORT names the port on the machine being shared; AS is the local port every other peer binds. Nothing is exposed to the internet - only members of the network reach it, and only on their own loopback. TUNNEL is the verb for a public URL.
The transfer is non-blocking. TRANSFER returns a status anchor immediately rather than waiting for the file to land. DIRECTIVE STATUS ?statusanchor reads the current state of that transfer - distinct from DIRECTIVE STATUS "id", which reports whether a machine is online. The verb tells them apart by what it is given: a status anchor, or a machine id.

SCREENSHOT and WAKE

Capture the Remote Screen
DIRECTIVE "personal-server" SCREENSHOT SET ?shot
AFTER EMIT ?shot
(* ?shot is the path to a real PNG in your namespace *)

The capture is written into your namespace as an ordinary PNG and the verb hands back its path. Nothing is base64, and nothing needs decoding: it is a file, so IMAGE LOAD, GRAPHIC, FILE SHARE and everything else that takes a file work on it directly.

A Screenshot You Can Open in a Browser
DIRECTIVE "laptop" SCREENSHOT SET ?shot
AFTER FILE SHARE ?shot SET ?link
AFTER EMIT ?link
AFTER EMIT `<img src="` & ?link & `">`
(* The URL points at the PNG itself — no wrapper page needed *)
Wake-on-LAN
DIRECTIVE "office-pc" WAKE "AA:BB:CC:DD:EE:FF"
(* An online agent sends the magic packet on its LAN to wake another machine *)
WAKE has a real network constraint. Wake-on-LAN packets are broadcast on the local network segment and do not route across the public internet. WAKE only succeeds if another already-online oql-client agent shares that machine's local network and can relay the magic packet locally.

TUNNEL — Hosting Without a Static IP

Tunnel traffic travels over the connection the agent already holds open to Ocalt — there is no second program to install and no port to open on the machine or the router. Requests arrive at the tunnel URL, are passed down that connection, answered by whatever is listening on the machine's local port, and streamed back.

A tunnel exposes a port on the target machine to the public internet through the agent's own outbound connection — no port forwarding, no static IP, works behind NAT and on mobile connections. Opening one returns a generated URL of the form https://<token>.tunnel.ocalt.com. The tunnel runs independently of the script that created it; closing it deletes the token, and the URL stops working immediately even if the machine is still online.

Exposes a port on the remote machine to the public internet through the agent's already-open outbound connection — no port forwarding, no static IP, works over mobile/cellular connections. The tunnel runs indefinitely once created, independent of the script that started it, and is closed by a separate, later call.

Start and End a Tunnel
DIRECTIVE "home-nas" TUNNEL PORT 3000 SET ?tunnel
AFTER EMIT ?tunnel("url")
(* Returns https://<token>.tunnel.ocalt.com — public traffic reaches localhost:3000 on the agent machine *)
Ending a Tunnel
DIRECTIVE TUNNEL END ?tunnel
Each tunnel gets its own subdomain. The token is the hostname, not a path, so a site behind the tunnel keeps working when its pages reference assets from the site root - /style.css and /img/logo.png resolve inside that tunnel rather than colliding at the top of a shared domain. Nothing is stored in the browser to make this work.
Auto-expiry. If the agent's connection drops and does not reconnect within 5 minutes, the tunnel is automatically torn down and its URL stops resolving.

Tunnel URLs are generated per tunnel and are not tied to your subdomains. Site Mode hosting is unaffected by tunnels: a subdomain always serves its own files, whether or not any tunnel is open.

REDIRECT on the Remote Agent

DIRECTIVE "id" REDIRECT "url" opens a URL on the remote agent machine's own local client — useful when the remote machine itself needs to open something locally, such as joining a video call from its own side. This is distinct from the normal REDIRECT verb, which redirects the visitor who triggered the script.

Two Different Redirect Targets in One Script
NEW VIDEO BRIDGE 2 PEERS SET ?room
AFTER DIRECTIVE "home-server" REDIRECT ?room("peers")(0)("connect_url")
(* Opens the URL on the remote agent's own local client *)
AFTER REDIRECT ?room("peers")(1)("connect_url")
(* Redirects the actual visitor who ran this script *)

File Access — Read & Write

Read and write files directly on the target machine. A path is the machine’s own path, used exactly as given, and reaches anything the agent’s operating-system user can read or write. The root folder in the agent’s configuration is for Site Mode only; it never applies to READ, WRITE, LIST, DOWNLOAD, UPLOAD or SERVE. Give absolute paths, since a relative path depends on the folder the agent was started in.

Write then Read a File
DIRECTIVE "personal-server" WRITE "/opt/app/config/app.json" CONTENT '{"mode":"live"}' SET ?w
AFTER DIRECTIVE "personal-server" READ "/opt/app/config/app.json" SET ?cfg
AFTER EMIT ?cfg("data")
(* WRITE creates parent folders as needed; READ returns file contents in data *)
Reading and Writing on the Remote Machine
DIRECTIVE "personal-server" READ "C:/app/config.json" SET ?cfg
AFTER EMIT ?cfg
Bridging Remote Logs into Local Storage
DIRECTIVE "personal-server" READ "/var/log/app.log" SET ?log
AFTER FILE WRITE "/mounted/logs/app.log" CONTENT ?log("data")

Browsing the Machine's Filesystem

LIST FROM returns the contents of a folder on the target machine; LIST DRIVES returns the machine's drives, which is how you find out what paths exist before listing one. Both are read-only and need no desktop session.

The desktop input verbs need a real session. MOUSE, KEYBOARD, POINTER, SCREENSHOT, VIEW and SCREEN act on a logged-in graphical session; on a headless machine they return a display error. LIST, LIST DRIVES, READ, WRITE, DOWNLOAD and UPLOAD need no session. On macOS the person grants Screen Recording and Accessibility once in System Settings before capture and input work.
List Drives, Then List a Folder
DIRECTIVE "personal-server" LIST DRIVES SET ?arrayofdrives
AFTER EMIT ?arrayofdrives(0)
AFTER DIRECTIVE "personal-server" LIST FROM "C:/" SET ?cdrivefolderitemarray
AFTER FOREACH ?cdrivefolderitemarray SET ?item
OPEN
  EMIT ?item("name")
CLOSE

LIST DRIVES returns an array of paths, each ready to pass to LIST FROM: on Linux, the mounted filesystems, such as "/" and "/boot/efi"; on Windows, the drive roots, such as C:\. It names the drives and nothing more. A drive's size and free space come from the machine itself, through EXEC, which runs /bin/sh on Linux and cmd.exe on Windows: df answers on Linux, and PowerShell's Get-PSDrive on Windows.

Size and Free Space of a Drive on Linux
DIRECTIVE "personal-server" EXEC "df -B1 --output=target,size,avail /" SET ?r
AFTER EMIT ?r("stdout")
(* Output, in bytes:
   Mounted on    1B-blocks       Avail
   /          103865303040 23160971264 *)
Each entry from LIST FROM carries name, is_dir, size and modified - the same shape FILE LIST returns for your own namespace, so the two can be walked by identical code.

SERVE — Expose a File Over the Tunnel

SERVE exposes a single file on the machine, without copying it anywhere. It returns a public url ready to use, and also the local port it bound, so the file can be paired with TUNNEL by hand when a script needs the port for something else. Nothing is installed and nothing is uploaded; the file is read from disk as it is requested.

Serve a File and Get Its URL
DIRECTIVE "laptop" SERVE "C:/media/clip.mp4" SET ?src
AFTER EMIT ?src("url")
(* Returns https://serve.ocalt.com/<token>.mp4 — the served file’s extension is part of the URL, so a browser or player picks the right handler. The file streams from the machine on demand *)
A token path, not a subdomain. A served file sits alone at the root of its URL - there is nothing else to navigate to - so SERVE uses serve.ocalt.com/<token>.<ext> - the token carries the served file’s extension so the URL ends in .mp4, .pdf and the like - while a TUNNEL, which fronts a whole site, gets its own subdomain instead.
Serve a File and Tunnel It Yourself
DIRECTIVE "laptop" SERVE "C:/media/clip.mp4" SET ?src
AFTER DIRECTIVE "laptop" TUNNEL PORT ?src("port") SET ?tunnel
AFTER EMIT ?tunnel("url")
(* The file is now readable at that URL, streamed from the machine on demand *)
Ranged requests are supported, so a video served this way can be seeked and played rather than only downloaded whole. The file is streamed in chunks — nothing is held in memory on either side, so size is not a constraint.

SCREEN — Display Geometry

SCREEN returns one entry per display, so a script can work out where it is pointing before it moves anything. Each entry carries width, height, x, y and primary.

Read the Displays, Then Click the Centre
DIRECTIVE "laptop" SCREEN SET ?screen
AFTER EMIT ?screen(0)("width")
AFTER EMIT ?screen(0)("height")
AFTER CALCULATE ?screen(0)("width") / 2 SET ?cx
AFTER CALCULATE ?screen(0)("height") / 2 SET ?cy
AFTER DIRECTIVE "laptop" POINTER X ?cx Y ?cy
AFTER DIRECTIVE "laptop" MOUSE "[Click]"
(* ?screen(1) is null when there is only one display *)

VIEW - The Screen, Live

How the feed is served. VIEW returns an object with url, token, format and display. The URL takes the same form a served file does, https://serve.ocalt.com/<token>.<ext>, the extension naming the stream: .mjpg is a live feed any browser plays as it is, in an <img> tag or opened directly. ON DISPLAY n picks a screen, and each display streams on its own, so every screen of a machine can be watched at once. A feed closes after 300 seconds with no viewer, or at DIRECTIVE VIEW END ?feed. To publish a feed further, to RTSP or HLS, pass its URL to NEW TRANSCODE STREAM.
One Display, Another, or Every One
DIRECTIVE "laptop" SCREENSHOT SET ?shot
(* the primary display *)

AFTER DIRECTIVE "laptop" SCREENSHOT ON DISPLAY 2 SET ?second
(* the second monitor *)

AFTER DIRECTIVE "laptop" SCREENSHOT ON DISPLAY 2 SET ?second
AFTER COUNT ?shots SET ?n
AFTER EMIT ?n & " displays captured"
(* one path per capture; ask for the display you want *)

Displays are numbered from 1, in the order SCREEN reports them, so a script can ask what is there before deciding what to capture.

A Display, in a Page of Your Own
DIRECTIVE "workstation" VIEW ON DISPLAY 2 SET ?feed
AFTER EMIT `<img src="` & ?feed("url") & `">`
(* Displays are numbered from 1, in the order SCREEN reports them *)
A feed is one video pipeline, so a feed is one display. To watch two screens, open a feed for each and put them side by side in your own page.

How long a feed lasts

A feed lives while it is being watched. Five minutes after the last time anything pulls from it, it closes and its URL stops working.

Idle is the right thing to measure, not age. A fixed expiry would cut off someone in the middle of watching, and would keep an abandoned tab transcoding long after everyone walked away. This way a feed you are using never dies under you, and a feed nobody is looking at stops costing anything.

Checking Whether a Feed Is Still Alive
DIRECTIVE "laptop" VIEW SET ?feed
AFTER EMIT ?feed("url")
AFTER EMIT ?feed("expires_after")
(* 300 — seconds of inactivity before it closes *)

(* Later, in another script *)
AFTER DIRECTIVE VIEW END ?feed
(* Or just stop watching: it closes on its own. *)
A link to someone’s live screen is a more serious thing to leak than a link to one old frame. It stops working on its own rather than lasting until somebody remembers to revoke it - and if you want it gone now, DIRECTIVE VIEW END closes it immediately.

Look, decide, act

Driving a Machine by Hand
(* Look. *)
DIRECTIVE "personal-server" SCREENSHOT SET ?before
AFTER FILE SHARE ?before SET ?link
AFTER EMIT ?link

(* Act, then look again. *)
AFTER DIRECTIVE "personal-server" POINTER X 400 Y 300
AFTER DIRECTIVE "personal-server" MOUSE "[Click]"
AFTER DIRECTIVE "personal-server" SCREENSHOT SET ?after
AFTER FILE SHARE ?after SET ?link2
AFTER EMIT ?link2

(* Two files, two moments — you can see exactly what the click did. *)
These need a desktop session. MOUSE, KEYBOARD, POINTER, SCREENSHOT, VIEW and SCREEN all act on a real display - injecting input into it or reading pixels from it. On a machine with no graphical session, such as a headless server, they return an error about not reaching a display. That is not something a script can work around: the machine needs a logged-in desktop for them to have anything to act on.
Input on a phone needs a one-time grant. On Android, MOUSE, KEYBOARD, POINTER MOVE and POINTER COORDINATES run through an accessibility service the person switches on once in Settings › Accessibility › Ocalt. Android has no other way for an app to drive input, and the switch cannot be flipped from code. Until it is on, those four return a needs-setup message the same way a headless desktop reports a missing session. A phone has no hardware pointer, so POINTER MOVE sets a virtual pointer and MOUSE "[Click]" taps where it sits.

Input Control — Mouse, Keyboard & Pointer

Beyond shell access, DIRECTIVE can drive the target machine's actual mouse and keyboard through the OcaltQL Client's input service. The client must be running on the target with input access — combined with SCREENSHOT, this makes a machine fully navigable from a script. Keys and buttons are written as [Name] tokens joined by +: "[Control]+[c]", "[MouseDown]+[Down]". The full grammar is below.

Key and Button Syntax

Keys and buttons are written as [Name] tokens. Combine them with + between brackets — [Control]+[c], [Control]+[Shift]+[Escape]. A quoted string inside the brackets is a literal character, which is how you send the bracket keys themselves: ["]"] is the ] key, ["["] is [. Single characters need no quotes: [a], [5], [/].

Typing is a sequence, not one key. KEYBOARD takes a whole instruction in one command, so a phrase costs one round trip rather than one per letter: "[Control]+[c]" is a chord, the modifiers held while the last key is tapped; "[h][e][l][l][o]" is five keys pressed one after another; "Hello there." is typed literally, character for character, including characters no key is named for; and the forms mix, as in "[Control]+[a]Replacement text[Enter]". A key the machine does not recognise raises an error you can catch, so a mistyped name can never fail quietly.
Type a Phrase, in One Command
DIRECTIVE "laptop" KEYBOARD "[Control]+[a]"
AFTER DIRECTIVE "laptop" KEYBOARD "Rewritten by a script.[Enter]"
AFTER DIRECTIVE "laptop" KEYBOARD "[Control]+[s]"

Named keys follow the platform key set: [Enter], [Escape], [Tab], [Space], [Backspace], [Delete], [Insert], [Home], [End], [PageUp], [PageDown], [Up] [Down] [Left] [Right] (or [ArrowUp] etc.), [F1]–[F20], [CapsLock], [NumLock], [ScrollLock], [PrintScreen], [Pause], [ContextMenu], media keys such as [VolumeUp], [VolumeDown], [VolumeMute], [MediaPlayPause], [MediaNextTrack], and numpad keys [Numpad0]–[Numpad9].

Modifiers: [Control] (or [Ctrl]), [Shift], [Alt], [Meta] (or [Win], [Cmd]). In a combination the last token is the key pressed; everything before it is held down and released after.

Mouse tokens: [Click], [RightClick], [MouseDown], [MouseUp], [Up] [Down] [Left] [Right] (relative movement), [ScrollUp], [ScrollDown]. Combine them the same way: [MouseDown]+[Down] drags downward.

Coordinates are absolute across the whole virtual desktop, not per-monitor. Your primary display's top-left corner is 0,0; a monitor to its right occupies x values beyond the primary's width, and one to its left uses negative x. POINTER COORDINATES reads back in the same space, so you can read a position, work out which display it falls on, and move relative to it.
Move and Read the Pointer
DIRECTIVE "personal-server" POINTER X 400 Y 300
AFTER DIRECTIVE "personal-server" POINTER COORDINATES SET ?coord
AFTER EMIT ?coord("x")
AFTER EMIT ?coord("y")
(* POINTER COORDINATES blocks for the agent's reply — ?coord("x"), ?coord("y") *)
Mouse Actions
DIRECTIVE "personal-server" MOUSE "[MouseDown]+[Down]"
AFTER DIRECTIVE "personal-server" MOUSE "[ScrollDown]"
(* Tokens: [Click] [RightClick] [MouseDown] [MouseUp] [Up] [Down] [Left] [Right] [ScrollUp] [ScrollDown] *)
Keyboard Input
DIRECTIVE "personal-server" KEYBOARD "[Control]+[c]"
AFTER DIRECTIVE "personal-server" KEYBOARD "[Enter]" SET ?ok
AFTER EMIT ?ok
(* Single keys or + combinations. Optional SET returns true/false for success *)
Screenshot-Driven Navigation
DIRECTIVE "personal-server" SCREENSHOT SET ?img
AFTER DIRECTIVE "personal-server" POINTER X 250 Y 480
AFTER DIRECTIVE "personal-server" MOUSE "[Click]"
AFTER DIRECTIVE "personal-server" KEYBOARD "[Enter]"
(* Capture, locate a target, move, click, type — the remote navigation loop *)

When Things Fail

Two conditions stop a script outright, because the script named something that cannot work: addressing a machine id that holds no slot, and calling DIRECTIVE REGISTER when every slot is in use. Both raise a fatal error naming the id and, for slot saturation, how long until a slot can be released.

Everything else is reported rather than fatal. A blocking command against an agent that is offline or stops responding waits until the command's ceiling of 300 seconds and then returns a timeout — the row stays on the hub, so if that agent reconnects later it still receives the work. A command the agent could not carry out comes back with ok false and an error describing why, which you can branch on. Use DIRECTIVE STATUS "id" before a long run if you would rather check that a machine is online first.

A tunnel is torn down if its agent stops reconnecting, and closing one revokes it centrally: the URL stops working immediately, whatever state the machine is in.

Rules

Rule Detail
No nestingCannot call DIRECTIVE inside another DIRECTIVE statement
Machine IDMust be a registered slot; the agent must be online except for WAKE, which targets an offline machine through an online relay
FilesystemDIRECTIVE READ/WRITE resolve paths on the remote machine's filesystem
CredentialsThe agent uses the same Ocalt credentials as the namespace that registered it
Machine slots4,096 on every plan - a released slot unlocks after 24 hours

The Machine's Location

Every registered machine reports approximate coordinates, lat and lon, resolved from the public address it checks in from. No permission prompt, no GPS - it is the same city-level accuracy a web server infers from an IP. Both are null until the machine has checked in since the hub last started, and for an address the location database does not cover.

Where a Machine Is
DIRECTIVE STATUS "home-nas" SET ?s
AFTER EMIT ?s("lat") & ", " & ?s("lon")

AFTER DIRECTIVE LIST SET ?machines
AFTER FOREACH ?machines("agents") SET ?m
OPEN
  EMIT ?m("machine_id") & " — " & ?m("lat") & "," & ?m("lon")
CLOSE
Coordinates come from the machine’s public address, so a machine behind a VPN reports the VPN’s location, and one with no internet route reports nothing. For a precise fix from a person’s device, use USER DEVICE in a script they load - that asks the browser, and the visitor consents.

A Local Server on the Machine

The agent can serve the machine’s root folder as a website on a port of its own. Static files come straight off local disk; an .oql file executes on Ocalt with the visitor’s request forwarded, exactly as Site Mode does. The same script behaves identically on a subdomain and on the machine.

Set the port in the agent’s configuration page at localhost:12345/config, alongside the root folder. Any free port works - including 80 - except 12345, which the agent’s own interface uses. Blank or 0 turns it off.
The local server listens on the machine’s network, not the internet. To reach it from outside, put a TUNNEL in front of it: DIRECTIVE "home-nas" TUNNEL PORT 80 SET ?t.

Using it as a development server

This is how you build a site without deploying anything. Point the agent’s root at the folder you are working in, give it a port, and open it in a browser - you are editing files on your own machine while real OcaltQL runs against your real namespace.

The Whole Setup
(* 1. Open the agent's own manager on the machine *)
    http://localhost:12345/config

(* 2. Set two fields:
       Root        /home/you/projects/myshop
       Local port  8080                       *)

(* 3. Write a script in that folder *)
    /home/you/projects/myshop/index.oql

(* 4. Open it *)
    http://localhost:8080/

No build step, no upload, no restart. The agent reads the file off disk on every request, so a refresh always shows what you last saved.

How a URL becomes a file

You open It serves
/index.oql in the root folder
/ordersorders.oql - an extensionless path falls back to .oql
/shop/shop/index.oql - a folder serves its index
/style.cssThe file itself, straight off local disk
/video.mp4The file, with byte ranges - so seeking in a player works

That is the same routing Site Mode uses on a subdomain, which is the point: a script that works here works there unchanged, because it is the same script running in the same place. Only the thing that fetched it differs.

What reaches your script

The agent forwards the visitor’s method, query string, body and cookies along with the script, so !GET, !POST, !REQUEST, !COOKIE and START SESSION behave exactly as they will in production. A form you test on localhost posts to the same code that will receive it live.

A Form You Can Test on localhost
(* index.oql *)
IF !REQUEST("method") IS IDENTICAL TO "POST"
OPEN
  EMIT "You sent: " & !POST("message")
CLOSE
OR
OPEN
  EMIT `<form method="post">
          <input name="message">
          <button>Send</button>
        </form>`
CLOSE
The script runs on Ocalt, not on your machine. Only the file is local. Your databases, your namespace files, your machines and your quota are all exactly what they will be in production - there is no second runtime to drift out of step, and nothing to install but the agent.
Anything outside the root folder is refused, and a path containing .. is rejected before it is looked at. Port 12345 cannot be used - that is the agent’s own manager.

Agent Setup

Install the oql-client application on the machine you want to reach. On first run it opens its own local manager at http://localhost:12345, where you enter your Ocalt credentials, a machine alias (the id you target in DIRECTIVE), and a local root folder, which is what Site Mode serves. The agent stores that config and connects outbound on its own — no inbound port forwarding, and nothing to open on your router.

The manager also shows a live activity log of every command the machine has run, and the agent can be set to start automatically at logon. Once configured, the machine is addressable from any OcaltQL script using its alias.

Full Verb Reference

Verb Description
DIRECTIVE "id" EXEC "cmd" SET ?rRun a shell command, blocking
DIRECTIVE "id" EXEC "cmd" BACKGROUND SET ?pidRun a shell command, non-blocking
DIRECTIVE "id" KILL ?pidTerminate a background process by its operating-system pid — the whole process group is signalled, so child processes die with it
DIRECTIVE "id" DOWNLOAD "remote" TO "local" SET ?hTransfer a file from the remote machine
DIRECTIVE "id" UPLOAD "local" TO "remote" SET ?hTransfer a file to the remote machine
<call> SET PROMISE ?jobRun a blocking directive call, such as EXEC, in the background — ?job is WAITING, collect with WAIT FOR. DOWNLOAD and UPLOAD never block and need no promise
DIRECTIVE "id" SCREENSHOT SET ?imgCapture the remote machine's screen
DIRECTIVE "id" WAKE "MAC"An online agent broadcasts a Wake-on-LAN packet on its LAN for the given MAC
DIRECTIVE "id" TUNNEL PORT n SET ?tunnelExpose a local port publicly, no port forwarding needed
DIRECTIVE TUNNEL END ?tunnelClose an active tunnel
DIRECTIVE REGISTER "id" SET ?regClaim a machine slot — fatal if every slot is in use
DIRECTIVE UNREGISTER "id" SET ?relRelease a slot, permitted once it is 24 hours old
DIRECTIVE "id" SERVE "path" SET ?srcServe one file straight off the machine - returns a public url plus the local port
DIRECTIVE "id" SCREEN SET ?sDisplay geometry per monitor — ?s(0)("width"), ("height"), ("x"), ("y"), ("primary")
DIRECTIVE "id" SCREENSHOT [ON DISPLAY n] SET ?pathCapture a display to a timestamped PNG in your namespace
DIRECTIVE "id" VIEW [ON DISPLAY n] SET ?feedOpen a live feed of a display, returning its URL
DIRECTIVE "id" REDIRECT "url"Open a URL in that machine's own default browser
DIRECTIVE "id" POLL n SECONDS|MINUTESCheck-in interval for one machine - 1 SECOND to 60 MINUTES, default 1 SECOND
DIRECTIVE PEERS "id" AND "id" SET ?networkGroup registered machines into one peer network value
DIRECTIVE NETWORK ?network TRANSFER FROM "id://path" TO "id://path" SET ?anchorMove a file directly between two peers - non-blocking, returns a status anchor
DIRECTIVE NETWORK ?network SHARE "id" PORT n AS n SET ?statusExpose one peer’s port on every other peer’s localhost
DIRECTIVE STATUS ?anchor SET ?progressRead a peer transfer's progress - buffering, done, or in progress
DIRECTIVE "id" LIST FROM "C:/" SET ?itemsList a folder on the machine - name, is_dir, size, modified
DIRECTIVE "id" LIST DRIVES SET ?drivesList the machine's drives as an array of paths: its mounted filesystems on Linux, drive roots such as C:\ on Windows
DIRECTIVE LIST SET ?aRegistered machines plus slot accounting — ?a("agents"), each with machine_id, online, last_seen, seconds_since_seen, lat and lon; ?a("used"), ?a("slots")
DIRECTIVE STATUS "id" SET ?statusA specific agent's liveness and location — online, last_seen, seconds_since_seen, lat, lon
DIRECTIVE "id" READ "path"Read a file at the machine’s own path — returns data and bytes
DIRECTIVE "id" WRITE "path" CONTENT "..."Write a file at the machine’s own path, creating its folders
DIRECTIVE "id" MOUSE "[Token]"Button, scroll or relative move, e.g. "[MouseDown]+[Down]"
DIRECTIVE "id" KEYBOARD "[Key]+[Key]"Send a key or chord, e.g. "[Control]+[c]"
DIRECTIVE "id" POINTER X n Y nMove the pointer to absolute coordinates
DIRECTIVE "id" POINTER COORDINATES SET ?cRead current pointer position — ?c("x"), ?c("y")