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 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.
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.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")
DIRECTIVE UNREGISTER "old-laptop" SET ?rel
AFTER EMIT ?rel
AFTER DIRECTIVE REGISTER "new-laptop" SET ?reg
AFTER EMIT ?reg
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
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.
DIRECTIVE "personal-server" POLL 1 SECONDS
AFTER DIRECTIVE "home-nas" POLL 5 MINUTES
AFTER DIRECTIVE "backup-box" POLL 60 MINUTES
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.
DIRECTIVE "personal-server" EXEC "apt-get update && apt-get install -y ffmpeg" SET ?r
AFTER EMIT ?r("stdout")
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.
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. *)
DIRECTIVE "personal-server" UPLOAD "/root/config.json" TO "/etc/app/config.json" SET ?upl
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.
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.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.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 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.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"
DIRECTIVE "personal-server" EXEC "./nightly-report.sh" SET PROMISE ?job
AFTER EMIT "report started"
AFTER WAIT FOR ?job SET ?r
AFTER EMIT ?r("stdout")
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.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"
CONSISTENT "backup_transfer" SET ?tid
AFTER DIRECTIVE STATUS ?tid SET ?p
AFTER EMIT ?p("state") & ": " & ?p("bytes") & " of " & ?p("total") & " bytes, " & ?p("percent") & "%"
DIRECTIVE PEERS "deviceid1" AND "deviceid2" AND "deviceid3" SET ?network
AFTER EMIT ?network(0)("id")
AFTER COUNT ?network SET ?n
AFTER EMIT ?n & " peers"
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
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.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
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.
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 *)
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 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.
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 *)
DIRECTIVE TUNNEL END ?tunnel
/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.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.
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.
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 *)
DIRECTIVE "personal-server" READ "C:/app/config.json" SET ?cfg
AFTER EMIT ?cfg
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.
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.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.
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 *)
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.
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 *)
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.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 *)
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.
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
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.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.
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 *)
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.
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. *)
DIRECTIVE VIEW END closes it immediately.Look, decide, act
(* 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. *)
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.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], [/].
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.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.
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.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") *)
DIRECTIVE "personal-server" MOUSE "[MouseDown]+[Down]"
AFTER DIRECTIVE "personal-server" MOUSE "[ScrollDown]"
(* Tokens: [Click] [RightClick] [MouseDown] [MouseUp] [Up] [Down] [Left] [Right] [ScrollUp] [ScrollDown] *)
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 *)
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 nesting | Cannot call DIRECTIVE inside another DIRECTIVE statement |
| Machine ID | Must be a registered slot; the agent must be online except for WAKE, which targets an offline machine through an online relay |
| Filesystem | DIRECTIVE READ/WRITE resolve paths on the remote machine's filesystem |
| Credentials | The agent uses the same Ocalt credentials as the namespace that registered it |
| Machine slots | 4,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.
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
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.
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.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.
(* 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 |
/orders | orders.oql - an extensionless path falls back to .oql |
/shop/ | shop/index.oql - a folder serves its index |
/style.css | The file itself, straight off local disk |
/video.mp4 | The 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.
(* 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
.. 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 ?r | Run a shell command, blocking |
DIRECTIVE "id" EXEC "cmd" BACKGROUND SET ?pid | Run a shell command, non-blocking |
DIRECTIVE "id" KILL ?pid | Terminate 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 ?h | Transfer a file from the remote machine |
DIRECTIVE "id" UPLOAD "local" TO "remote" SET ?h | Transfer a file to the remote machine |
<call> SET PROMISE ?job | Run 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 ?img | Capture 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 ?tunnel | Expose a local port publicly, no port forwarding needed |
DIRECTIVE TUNNEL END ?tunnel | Close an active tunnel |
DIRECTIVE REGISTER "id" SET ?reg | Claim a machine slot — fatal if every slot is in use |
DIRECTIVE UNREGISTER "id" SET ?rel | Release a slot, permitted once it is 24 hours old |
DIRECTIVE "id" SERVE "path" SET ?src | Serve one file straight off the machine - returns a public url plus the local port |
DIRECTIVE "id" SCREEN SET ?s | Display geometry per monitor — ?s(0)("width"), ("height"), ("x"), ("y"), ("primary") |
DIRECTIVE "id" SCREENSHOT [ON DISPLAY n] SET ?path | Capture a display to a timestamped PNG in your namespace |
DIRECTIVE "id" VIEW [ON DISPLAY n] SET ?feed | Open 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|MINUTES | Check-in interval for one machine - 1 SECOND to 60 MINUTES, default 1 SECOND |
DIRECTIVE PEERS "id" AND "id" SET ?network | Group registered machines into one peer network value |
DIRECTIVE NETWORK ?network TRANSFER FROM "id://path" TO "id://path" SET ?anchor | Move a file directly between two peers - non-blocking, returns a status anchor |
DIRECTIVE NETWORK ?network SHARE "id" PORT n AS n SET ?status | Expose one peer’s port on every other peer’s localhost |
DIRECTIVE STATUS ?anchor SET ?progress | Read a peer transfer's progress - buffering, done, or in progress |
DIRECTIVE "id" LIST FROM "C:/" SET ?items | List a folder on the machine - name, is_dir, size, modified |
DIRECTIVE "id" LIST DRIVES SET ?drives | List the machine's drives as an array of paths: its mounted filesystems on Linux, drive roots such as C:\ on Windows |
DIRECTIVE LIST SET ?a | Registered 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 ?status | A 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 n | Move the pointer to absolute coordinates |
DIRECTIVE "id" POINTER COORDINATES SET ?c | Read current pointer position — ?c("x"), ?c("y") |