On this page
Events
Atlas has seven runtime callbacks: five for the script lifecycle, video, and controller input, and two network events that run only while a script captures network traffic. Video frames, controller packets, and network packets arrive on separate schedules, so each event has a specific job and a specific set of APIs it may use.
The exact lifecycle
- Load: Atlas parses, validates, compiles, and runs global initializers once for the loaded source.
- Run:
on_startruns first, followed immediately byon_connect. - Video: Each accepted capture frame increments
frame, refreshes frame metadata, and runson_frame. - Controller: Each queued physical controller packet refreshes
inputand runson_input. - Network (optional): After
network.capture(filter)starts capture, each captured packet runson_network_recvoron_network_send. - Stop: If the script was running,
on_stopruns before Atlas neutralizes script output and clears script feedback and overlays. Network capture ends afteron_stop.
on_connect behavior: it runs once after
on_start for each Run. The native runtime does not currently dispatch it again
when the detected controller family changes. Read the current family in
on_input when a live change matters.
Global initializers belong to the Load phase, not the Run phase. Reloading the script creates its initial state again. Re-running an already loaded VM without loading it again does not repeat those initializers.
Event names are exact and case-sensitive. Only on_start,
on_connect, on_frame, on_input,
on_stop, on_network_recv, and on_network_send
receive runtime slots. The native analyzer currently allows an unknown
event name, but the compiler never dispatches it; the editor normally reports that typo.
Declaring the same event twice produces a native warning and the last declaration wins.
Event availability
Controller access is a lexical rule. A controller call must appear directly inside
on_input; putting it in a helper function is rejected even when that helper is
called only by on_input.
| Event | When it runs | Controller APIs | Typical work |
|---|---|---|---|
on_start |
Once at Run, before on_connect |
No | Configure models and vision tools; initialize run state; log startup |
on_connect |
Once immediately after on_start |
No | Inspect system.controller and record the initial family |
on_frame |
Once per processed capture frame | No | Read vision results, update shared decisions, and draw overlays |
on_input |
Once per queued controller input packet | Yes | Read the live pad and override only the output fields needed for this packet |
on_stop |
Once when a running script stops | No | Log custom shutdown state; optional non-controller cleanup |
on_network_recv |
Once per captured incoming packet, only while capture is active | No | Inspect, edit, or drop the packet with packet.* |
on_network_send |
Once per captured outgoing packet, only while capture is active | No | Inspect, edit, or drop the packet with packet.* |
The restricted roots are input, output, rumble,
lightbar, gyro, accel, ps5,
ps4, and xb. The one exception is
input.time, which may be read in any event or function. Constants such as
controller.ps5, host.cronuszen, and
vision.directml are not live controller reads and remain available anywhere.
packet follows the same lexical rule for the two network events: it must
appear directly inside on_network_recv or on_network_send, and a
helper function that uses it is rejected with atlas.packet_outside_network_event.
network.* may appear in any event or function, but
network.capture() starts capture only while the script is running; see
Where capture can start.
Event parameters
The recommended spelling is parameterless: event on_input() { }. Atlas also
accepts the compatibility spelling event on_input(input, output) { }. Those
names are not populated local parameters; input and output work
because they are built-in namespace roots. Other events must be parameterless.
on_start
Use on_start for work that should happen once after the loaded module has been
initialized. Model source values may be created globally; the live detector can then be
configured here. The configuration call returns a typed value that should be retained.
model sourceModel = models.set("models/object_detector.onnx");
model detector;
event on_start() {
detector = vision.model(
sourceModel,
["object"],
null,
0.55,
true,
vision.directml
);
system.log("Detector configured");
}
Do not read input, write output, or send controller feedback here.
The controller packet stream has its own callback.
on_connect
on_connect follows on_start during Run. Use
system.controller, because input.controller is restricted to
on_input. The family value may be controller.ps5,
controller.ps4, controller.xb (any XInput or Xbox controller), or
controller.unknown in the current host path.
event on_connect() {
system.log("Initial controller: " + system.controller);
if (system.controller == controller.ps5) {
system.log("PlayStation 5 controller selected");
}
}
on_frame
This is the video callback. Read typed vision values, update globals that
on_input can consume, and redraw frame-owned shapes. Atlas clears all
ui--prefixed drawings at the beginning of each frame, so rectangles, lines,
circles, and printed text should be emitted again while they should remain visible.
ui.status uses a separate persistent status entry.
bool objectVisible = false;
event on_frame() {
target hit = detector.target;
objectVisible = hit != null && hit.confidence >= 0.75;
if (hit == null) {
ui.status("Waiting for an object");
return;
}
ui.rect(
hit.box.x,
hit.box.y,
hit.box.width,
hit.box.height,
rgb.lime,
2,
false
);
ui.status(string.format("Confidence {}", hit.confidence));
}
Video and input rates are independent. Never assume one on_frame occurs for
each on_input, or vice versa.
on_input
This is the only event that may read or modify the live controller. The VM begins each invocation from the real incoming state, records the fields touched by the script, and emits a minimal patch. Every untouched button, axis, trigger, touch point, and motion value continues through normal passthrough.
event on_input() {
// Qualified platform members are button-name strings.
bool modifierHeld = input.held(ps5.l1);
// Override Cross for this packet. Other controls pass through.
output.set_button(ps5.cross, modifierHeld && objectVisible);
// input.get returns live numeric state: 0..100 for this trigger.
if (input.get(ps5.l2) > 50) {
output.set_trigger(ps5.r2, 100);
}
}
output.set_button every packet with the desired boolean value.
Helpers may calculate, but may not touch the controller
function ScaleAxis(value) {
return math.clamp(value * 1.25, -100, 100);
}
event on_input() {
var adjusted = ScaleAxis(input.right_x);
output.set_stick_x("right", adjusted);
}
A helper containing input.right_x or output.set_stick_x would fail
validation. Pass ordinary values into the helper and keep the physical read and write in
on_input itself.
on_stop
on_stop runs only when a script that was actually running is stopped. It may
log state or perform non-controller cleanup, but the analyzer does not permit controller
namespaces here. After the callback, Atlas sends a full output clear, clears script-owned
rumble, player LEDs, and lightbar ownership, and removes script overlays.
event on_stop() {
system.log("Script stopped");
ui.clear();
ui.status("");
}
output.clear() in on_stop. It is outside the
permitted controller context and is unnecessary because the host performs its own final
clear after the callback.
Network events
on_network_recv and on_network_send let a script inspect, edit,
or drop network packets. They are optional: a script that does not declare them never
touches the network, and nothing else in Atlas needs the setup described here.
The two events run only after network.capture(filter) has started capture.
Declaring them alone captures nothing. Capture uses WinDivert, a separate packet-capture
driver, and a small Atlas helper that runs with administrator rights and only moves
packets. Your script still runs inside Atlas.
Before a script can capture
- WinDivert 2.2.2 (x64), installed by you. Atlas does not include or download WinDivert. Get it from the official release page and put
WinDivert.dllandWinDivert64.sysin%ProgramFiles%\Atlas\WinDivert, or in another folder you choose when Atlas asks. - A folder only administrators can change. It must be a real folder, not a junction or link, on a local fixed drive. Atlas checks it and every folder above it: each must be owned by Administrators, SYSTEM, or TrustedInstaller, and no other account may be able to change it, delete it, or add files to it. The WinDivert folder also may not let other accounts create subfolders, and the two files may not let other accounts change them. A folder under Program Files meets these rules. Atlas only checks; it never creates or repairs a folder, and it refuses an unsafe one with the reason.
- The official files. Both files must match the SHA-256 of the official 2.2.2 release.
- Administrator approval. The first
network.capture()in each Run opens a Windows administrator (UAC) prompt forAtlas.NetworkBroker.exe, the Atlas helper. Later calls in the same Run reuse it. Stopping the script closes the helper, so the next Run asks again. - Interfaces. Capture covers all network interfaces. The Atlas window has no interface selector yet, so
network.interfacenormally reads"All interfaces".
When you Run a script that declares on_network_recv or
on_network_send and the WinDivert folder does not pass these checks, Atlas
shows a Set up WinDivert dialog. It names the folder it checked and what
is wrong, then offers three choices: Yes opens a folder picker,
No opens the official WinDivert download page, and Cancel
does nothing. Atlas checks a picked folder again before it saves it, and explains a refusal.
WinDivert is not set up, so on_network_recv and on_network_send will not fire.
and runs the script anyway. network.capture() still asks for administrator
approval; the helper then refuses to load WinDivert, network.active stays
false, and network.error says what is missing. After you download
WinDivert, stop and run the script again.
network.error reads
Network capture needs administrator approval, and the prompt was declined. Restart the script to try again.
Later network.capture() calls in the same Run return false without
asking again. The rest of the script keeps running.
Choosing packets with a filter
network.capture(filter) takes a filter in the
WinDivert filter language.
Only packets that match it reach your events; every other packet flows normally without
involving Atlas. Atlas applies the same filter to this PC's own traffic and to traffic
forwarded through this PC.
| Filter | Diverts |
|---|---|
"udp and udp.DstPort == 9000" | UDP packets sent to port 9000 |
"udp.DstPort >= 30000 and udp.DstPort <= 30020" | UDP packets sent to a port range |
"tcp.DstPort == 443 or tcp.SrcPort == 443" | TCP traffic on port 443 in both directions |
"ip.DstAddr == 192.0.2.10 or ip.SrcAddr == 192.0.2.10" | IPv4 traffic to or from one address |
"" | Every packet |
- A field test is false for a packet without that header, so
tcp.DstPort == 443never matches UDP. - A filter may be at most 2048 characters of printable ASCII (tabs and line breaks count as spaces) with balanced parentheses.
network.capture()returnsfalsefor a filter that breaks these rules, andnetwork.errorsays which rule. - WinDivert checks the grammar itself when capture starts. A filter it cannot compile leaves
network.activefalse, andnetwork.errorstarts withCapture filter error:. - Every matching packet waits for your script before it continues, so keep the filter narrow. An empty filter sends all of this PC's traffic through the script.
network.capture() returns true as soon as Atlas accepts the
request; the prompt and WinDivert start in the background. In the event that called it,
network.active is still false. Read network.active,
and any error from starting in the background, from a later event, such as
on_frame. Calling network.capture() again replaces the filter, and
the counters restart when the new capture begins.
When Atlas refuses the request itself, network.capture() returns
false and network.error already holds the reason in the same
event. It keeps that reason in later events until a request is accepted or the script is
loaded or run again, unless capture reports a newer error first.
Where capture can start
network.capture() starts capture only while the script is running. A function
behaves like the event that called it.
| Where | network.capture() |
|---|---|
on_start, on_connect, on_frame, on_input | Starts or replaces capture. |
on_network_recv, on_network_send | Replaces the filter of the running capture. |
| Global initializers | Returns false. They run when the script is loaded, before Run. |
on_stop | Returns false. The script has already stopped, and capture ends right after on_stop. |
In the two refused cases, network.error reads
Network capture can only start while the script is running.
network.stop() and the network.* status members work everywhere.
Which event runs
| Captured packet | Event |
|---|---|
| Sent by this PC | on_network_send |
| Arriving at this PC | on_network_recv |
| Forwarded through this PC (for example, for a console that shares this PC's connection) and leaving through the selected interface; with all interfaces, the interface that carries the default route | on_network_send |
| Any other forwarded packet | on_network_recv |
Inside the event, packet.forwarded, packet.outbound,
packet.loopback, and packet.ipv6 tell you how the packet was
captured. A packet whose event the script does not declare passes unchanged.
What the events may use
- Available:
packet.*(only here),network.*,math,string,system, your globals, and your functions, as long as those functions do not usepacket. - Not available: the controller roots, exactly as in
on_frame.input.timeis the only controller value a network event may read. - One at a time: packets are decided one after another, and never while another event such as
on_frameoron_inputis running. Globals are therefore safe to share, but slow work in any event also delays packets. - Logging:
system.logwrites one line per call. Limit how often a network event logs, or a busy connection floods the log.
The API reference lists every
network.* and packet.* member.
Passing, dropping, and editing
- Pass is the default. A packet the event leaves alone continues byte for byte.
- Drop with
packet.drop(). The packet is not delivered. A drop takes precedence over any edits, andpacket.pass()later in the same event cancels it. - Edit with
packet.set_byte,packet.set_bit,packet.write,packet.set_bytes,packet.copy_from, or theraw_*writers. Edits happen in place: a script cannot make a packet longer or shorter. Atlas recalculates the checksums of an edited packet before sending it. - Invalid edits are not sent. If an edited packet is no longer a valid IP packet of the same version, its length field claims more bytes than it has, or WinDivert refuses to send it, Atlas sends the original packet instead and counts it in
network.failed_open.
Slow scripts and errors fail open
Your connection never waits on a script that cannot answer. In each of these cases the
original packet continues unchanged, and network.failed_open counts it for the
current capture:
- No decision arrives within 50 milliseconds.
- The event stops with a runtime error, such as the call-depth limit, or using up the instruction budget. Each packet gets its own budget. Atlas writes the error to the session log; see Diagnostics.
- 4096 packets are already waiting for a decision.
- An edit cannot be sent, as described above.
Packets still waiting when capture stops or is replaced also continue unchanged. Atlas
counts a packet as lost only when WinDivert refuses to put it back on the network. It then
logs Network capture could not put a captured packet back on the wire. once for
that capture, and the number of lost packets when the capture ends.
When capture stops
network.stop()stops capture. Packets still waiting pass unchanged, and a laternetwork.capture()in the same Run starts again without another prompt.- Stopping the script runs
on_stopfirst, so it can still read thenetwork.*counters, but it cannot start a new capture. Atlas then stops capture, passes any waiting packets unchanged, and closes the helper. - Losing authorization for a protected Library script stops the script and ends its capture too.
- If Atlas closes or loses its connection to the helper, the helper passes the packets it is holding, stops capturing, and exits.
var sent = 0;
var received = 0;
event on_start() {
// Divert only UDP traffic to or from port 9000.
if (!network.capture("udp.DstPort == 9000 or udp.SrcPort == 9000")) {
system.log("Network capture could not start");
}
}
event on_network_send() {
sent += 1;
// Drop empty datagrams; every other packet passes unchanged.
if (packet.payload_length == 0) {
packet.drop();
}
}
event on_network_recv() {
received += 1;
}
event on_frame() {
if (network.error != "") {
ui.status("Capture unavailable: " + network.error);
} else if (network.active) {
ui.status(string.format("UDP 9000: {} sent, {} received", sent, received));
}
}
event on_stop() {
system.log(string.format("{} dropped, {} failed open", network.dropped, network.failed_open));
}
More complete scripts are in the Network recipes.
Frame and timing values
| Value | Exact behavior | Use it for |
|---|---|---|
frame |
Resets to 0 at Run and becomes 1 for the first dispatched video frame. | Frame counters, visual cadence, periodic video work |
time_ms |
Milliseconds since the native script runtime object was created, not since the latest Run. | Monotonic elapsed comparisons where the absolute zero does not matter |
input.time |
0 initially, then a smoothed 0.9-EMA of positive packet intervals; individual samples are capped at 50 ms. | Stable per-input integration such as rate-based stick adjustments |
system.width / system.height |
Latest captured frame dimensions in pixels; 0 before valid frame metadata arrives. | Converting normalized target coordinates to pixels |
var accumulated = 0;
event on_input() {
if (input.held("r1")) {
accumulated += input.time;
} else {
accumulated = 0;
}
}
Schema event index
The cards below are generated from the reference schema so every callback entry in the reference schema is represented. The lifecycle and restriction notes above describe the verified native runtime when a schema description is broader than the current implementation.
on_startRuns once when the script starts. Use it to configure vision tools, model paths, and starting values.
event on_start() { }on_connectRuns once immediately after on_start for each script Run, using the controller family known at startup. It is not retriggered when the controller family later changes. Controller input/output is not available here; use system.controller for the startup family and on_input for controller packets.
event on_connect() { }on_frameRuns every frame for video processing only: vision.* and ui.*. Controller input, output, feedback, and motion are not available here; use on_input for the pad.
event on_frame() { }on_inputRuns for each controller input packet and is the only place controller APIs (input, output, rumble, lightbar, gyro, accel, platform views) are available. The real packet is forwarded automatically; override only the fields you change for this one packet. The parser accepts zero, one, or two declared parameter names for compatibility, but those names are cosmetic and receive no values; always use the global input and output roots.
event on_input() { }on_stopRuns once before the worker stops. Controller APIs are unavailable here; use it for UI, logging, or other non-controller cleanup. After it returns, Atlas automatically neutralizes controller output, clears rumble and player LEDs, releases lightbar ownership, and clears script overlays.
event on_stop() { }on_network_recvRuns for each captured packet arriving at this PC, and for forwarded packets that are not leaving through the selected interface, while network capture is active. Use packet.* to inspect or edit it, and packet.drop() to block it.
event on_network_recv() { }on_network_sendRuns for each captured packet leaving this PC, and for forwarded packets leaving through the selected interface (the interface that carries the default route when capturing all interfaces), while network capture is active. Use packet.* to inspect or edit it, and packet.drop() to block it.
event on_network_send() { }