Execution Contexts
The same iXML file can behave differently depending on how it was started. Identity, available globals, and HTTP behavior all come from the execution context, not from the file. Most app bugs that survive compilation are context bugs: code that works in zeysdk run fails in a weblet, or a service that works with a token rejects a browser call.
Identify the context first, then read the row that applies. Test in that same context. A pass in one context says little about another.
How to read the evidence labels
| Label | Meaning |
|---|---|
| Reference | Stated in the official iXML, SDK, or API reference |
| Source-confirmed | Read in the platform or SDK source for this release. It describes behavior at contract level and has not been executed for this page. A later platform release can change it |
| Logged | Dated execution result in the guide's validation log |
| Reported | Field observation that was not independently verified. Check it in your target |
Context matrix
| Context | Identity and auth source | Inputs and globals | Output and HTTP | Side effects | How to test it |
|---|---|---|---|---|---|
| REST API (external client, for contrast) | Token sent by the client, or the session cookie of a signed-in browser | The documented request only. Your iXML does not run here | JSON as defined by the OpenAPI reference | Record writes can fire entity hooks | HTTP request against a sandbox, or the zeyos CLI |
| Weblet, framed or page | Browser session cookie. No valid session means no execution | REQUEST, GET, POST, FILES, SERVER, SELFURL, VIEW, SELFID, SELFNS, APPSETTINGS. Record context arrives as request parameters such as $REQUEST.id | Your output and headers go to the frame | No implicit transaction. Use db:transaction | Open the real view while signed in |
Weblet in a .form view | Runs while the page is built for the signed-in user | ENTITY, ID, DATA, EXTDATA, TAGS, FORM, SELFID, SELFNS, APPSETTINGS. No REQUEST. New-record dialogs differ: no FORM, and normally no record variables | Form controls are collected. Plain output is discarded | Form data is saved by the host page | Open the record form and save it |
REMOTECALL service | Bearer token of the access or legacy type, read into AUTH. The cookie is ignored. A configured access key is a separate branch | AUTH when authenticated, REQUEST, GET, POST, FILES, SERVER, SELFURL, SELFID, SELFNS, APPSETTINGS. Inside rest:resource: route parameters, plus query arguments or POST form fields as locals, which can hide these globals | Your output, headers, and status go to the caller. CORS is open | No implicit transaction. Unauthenticated calls can still run, even for a restricted application | HTTP request with a token, without one, with an invalid one, and with forged query arguments |
zeysdk run | SDK token of a developer. No end-user session | SELFID, SELFNS, APPSETTINGS, DEBUG. No AUTH, REQUEST, SELFURL, or VIEW | Output, status, and headers are returned as a report | Real. Writes, mail, and external calls all happen | Only for the code path itself, on a sandbox |
TIMING service | Scheduler. No user | SELFID, SELFNS, APPSETTINGS | Discarded. Errors go to the platform log | No implicit transaction. One concurrent run per service | Wait for the schedule and inspect the effects |
| Entity hook | Runs inside the operation that changed the record. No AUTH | ENTITY, ID, and event-specific values, plus SELFID, SELFNS, APPSETTINGS | Discarded | Inside the record transaction. An error aborts the operation | Trigger the real entity event |
The globals, auth branches, and output handling in this table are Source-confirmed. Asset types and the REMOTECALL and TIMING metadata are Reference.
APPSETTINGS is the one input every application context shares. Everything else must be checked per context. The global names above are the iXML forms. Zymba has its own access syntax, so check ref/zymbadoc.md for Zymba assets.
Weblets
A weblet's type decides how the browser reaches it. Its view decides where it appears. ZeySDK 3.3.0 accepts these types:
| Type | Presentation (Source-confirmed) |
|---|---|
INTEGRATED | Menu entry that opens a page inside the ZeyOS shell |
STANDALONE | Menu entry that navigates the same tab |
DETACHED | Menu entry that opens a new tab |
POPUP_FRAMED, POPUP_PLAIN | Menu entry that opens a dialog with a frame |
EMBEDDED_FRAMED, EMBEDDED_COLLAPSED, EMBEDDED_PLAIN | Frame inside the view, with or without a collapsible heading |
All of these reach the same weblet endpoint, with one exception: a weblet registered on a view ending in .form is not a page. ZeyOS runs it on the server while it builds the form, so it must be iXML and it gets the record variables directly. Choose the path before you write code, because $REQUEST and $ID are not interchangeable across the two.
The endpoint requires a valid session. Without one it answers 401. An inactive weblet or application answers 403, as does a restricted application the user has no permission for. A weblet with a configured url redirects to that URL instead of running code.
view controls placement, not access. A weblet's URL is reachable by any signed-in user, regardless of its view, unless the application is restricted and the user lacks permission for it.
$REQUEST.id is an ordinary query parameter. The session proves who the user is, not that this user may see that record. Check access before you use the value in a query. See Weblets for an example and for the limits on requests that change data.
REMOTECALL services
The service URL has this form, where <base> is your instance URL:
<base>/remotecall/<app>.<service>/<route>
$SELFURL holds the numeric form of the same address, <base>/remotecall/~<ID>.
These are the corrections most often needed (Source-confirmed, with the SDK access-key flag as Reference):
- The browser session cookie does not authenticate a
REMOTECALLcall. The platform's auth step reads only theAuthorizationheader. - A valid token populates
AUTH. It holdsuser,application, andlang. Access tokens and legacy tokens are accepted. SDK and API tokens are not. AUTH.usermeans any valid token. It does not show which application the token was issued to. CompareAUTH.applicationwhere that matters.- An unauthenticated call can still execute. A missing token and an invalid token both leave
AUTHunset, and your script runs anyway. The platform does not answer 401 for you. Every protected handler must authorize explicitly. A restricted application does not change this: its permission check applies only when a user was identified, so an anonymous call still runs. - An access key is a separate branch. If the service has an
accesskey, the platform rejects a call without the key with 401. On successAUTHholds the key, not a user, so$AUTH.useris empty. - CORS is not authorization. Every
REMOTECALLresponse sendsAccess-Control-Allow-Origin: *. That header controls which pages may read a response. It does not restrict who can call the service. - A JSON body does not fill
REQUEST. Read it withvar_bodyonrest:resourceand decode it insidetry.
Bind globals inside rest:resource
Inside a handler, the request can create local variables, and locals hide globals (Reference). Query arguments become locals. For POST, form fields do instead (Source-confirmed). A global you read without binding it is therefore under the caller's control.
This matters most for authorization. Here ?AUTH[user]=1 passes the check with no token:
<!-- Wrong: a query argument can supply AUTH -->
<rest:resource route="/greeting/:name" method="GET">
<is var="AUTH.user" type="empty">
<set var="return">401</set>
<return />
</is>
</rest:resource>
<!-- Right: AUTH now refers to the platform value -->
<rest:resource route="/greeting/:name" method="GET">
<use var="AUTH" />
<is var="AUTH.user" type="empty">
<set var="return">401</set>
<return />
</is>
</rest:resource>
The general rule: inside rest:resource, any platform global (AUTH, APPSETTINGS, REQUEST, and so on) can be replaced by the request. Bind each one with use as the first child, before reading it. Do the same for your own globals, including resource controllers, macros, and shared arrays.
- A check before
rest:serveris not sufficient. It reads the realAUTH, but$AUTH.*inside the handler can still be forged, for example$AUTH.lang. - Functions are not affected. A function that the handler calls has its own context, so
$APPSETTINGSinside a resource function reads the real global. - Route parameters win over query arguments of the same name.
- Any local you read before assigning it is also caller-controlled. Assign your locals first.
This behavior was reproduced in a local run of the iXML engine with stand-in globals. Add a forged-argument test to every protected service. See Checkpoint 4.
The session command reads the cookie on its own, independently of the auth step. If a browser sends the cookie to a REMOTECALL URL, session may return that user while AUTH stays unset. Do not build authorization on this. It has not been verified at runtime, it skips the restricted-application check, and a cookie-authorized service is exposed to cross-site request forgery.
Calling your own service from a weblet
The browser may send its cookie with the call, but the auth step ignores it, so a weblet frame that calls its own REMOTECALL service arrives unauthenticated. Do not paper over this by accepting anonymous calls or by shipping a long-lived token to the browser.
First ask whether the browser needs to call a service at all. The weblet already runs server-side in the user's session and can call the same resource directly, as the minimal app does.
If the browser must call the service, that is a security design and it needs review. This guide does not publish a recipe. The design must define:
- Principal binding: which user the credential represents, set on the server from the session, never from a request parameter.
- Route binding: which routes, methods, and records it permits.
- Expiry: a short lifetime, checked on the server.
- Replay policy: whether reuse is allowed and how it is detected or limited.
- Server-side key handling: any signing key stays in instance settings and never reaches the page, the logs, or a response.
- Verification: tests for a missing, expired, tampered, wrong-user, and wrong-route credential, each run through a real HTTP request.
zeysdk run
zeysdk run is console execution with application globals. It is not a simulated weblet and not an inbound HTTP request.
- It sends your local file to the instance and executes it there, so it can run code you have not pushed. Anything that file includes resolves to the deployed assets on that instance.
- It runs with the app's namespace and settings, so
$APPSETTINGSand.nameincludes work. - There is no session, no
AUTH, noREQUEST, noSELFURL, and noVIEW.sessionreturns NULL. - Console requests use PUT, so
GETandPOSTroutes inrest:serverdo not match. Logged: on 6 February 2026, an inline REST example returned HTTP 404 andnull. - Effects are real. The console has no dry-run mode.
Use run to check a function or a query shape. Do not use it to conclude that a weblet, service route, or hook works. For compile-only checks, use zeysdk compile. See Verification Checkpoints.
TIMING services
The scheduler calls the service by schedule and interval. schedule is the start minute of the day, from 0 to 1439, and interval is the repeat period in minutes. With an interval of 1, schedule is ignored. The platform prevents concurrent runs of one service. All of this is Source-confirmed. Older ref/sdk pages show a 0 to 2359 time format, which the current SDK rejects. A skipped run is logged, not queued, so a job must be able to resume on its next run.
There is no user and no request. Log what the job did, because output is discarded.
Entity hooks
Hook types are AFTER_CREATION, BEFORE_MODIFICATION, AFTER_MODIFICATION, AFTER_CREATION_MODIFICATION, BEFORE_DELETION, and AFTER_DELETION. Each needs an entity (Reference).
Inputs depend on the event (Source-confirmed):
| Event | Inputs besides ENTITY and ID |
|---|---|
| Creation | NEW, NEW_EXTDATA, NEW_TAGS, MOD, MOD_EXTDATA, MOD_TAGS_ADD. No OLD |
| Modification | All of the above, plus OLD, OLD_EXTDATA, OLD_TAGS, MOD_TAGS_REMOVE |
| Deletion | OLD, OLD_EXTDATA, OLD_TAGS only |
AFTER_CREATION_MODIFICATION runs for both creation and modification, so check OLD before using it.
A hook runs inside the transaction of the change that triggered it (Source-confirmed). This has three consequences:
- An uncaught error aborts the user's operation, including from an
AFTER_*hook. - Slow work in a hook slows every save of that entity. Move heavy work to a
TIMINGservice. - External side effects such as mail or HTTP calls are not rolled back with the transaction.
While a modification hook runs, a further change to the same record does not fire modification hooks again (Source-confirmed).
Includes and JSON resources
include loads a resource by default. Use type to load a service or a weblet (Reference):
<include id=".Greeting" />
<include id=".api" type="service" />
<include id=".hello" type="weblet" />
The leading dot addresses an asset in the current app. It expands to the app identifier, so .Greeting in example.minimal means example.minimal.Greeting. This form is Source-confirmed and used throughout real apps, but ref/ixmldoc.xml does not describe it. It fails in a script that has no app namespace.
What a resource returns depends on its MIME type, not its extension (Source-confirmed):
| Resource MIME type | include returns |
|---|---|
application/ixml+xml, text/x-zymba | The executed code's result |
application/json | Decoded value |
application/xml, text/xml | Parsed tree |
text/csv, text/tab-separated-values | Parsed rows |
| Other text types | Text, with $name replaced from the include's parameters |
The SDK derives the MIME type from the extension when you add a file, which is why the two usually agree.
A JSON resource is already decoded, so decode:json on it is a mistake:
<!-- Wrong: the value is already an array -->
<include id=".defaults" var="raw" />
<decode:json var="defaults">$raw</decode:json>
<!-- Right -->
<include id=".defaults" var="defaults" />
<output>$defaults.title</output>
Session locale
The session command returns an array, or NULL when there is no session (Reference). The reference does not list its keys.
The user's locale is in data.lang (Source-confirmed, and Reported from an app build in November 2026). A top-level lang key is not part of the contract, and ref/dbref.json lists no lang column on users.
<session var="session" />
<set var="lang">$session.data.lang</set>
In a REMOTECALL service, use $AUTH.lang. Always handle an empty value.
session returns NULL only when there is neither a session user nor a valid cookie. That makes the result context-dependent: a hook fired from a signed-in page sees that user, while the same hook fired by a TIMING service does not. Do not rely on session in hooks or in REMOTECALL.
Common mistakes
- Treating a
zeysdk runpass as proof that a weblet or service route works. - Assuming a
REMOTECALLhandler is protected because the platform did not return 401. - Reading
$AUTHor$APPSETTINGSinsiderest:resourcewithout binding it withuse. - Treating
viewas an access control. - Reading
$REQUEST.idin a.formweblet, or$IDin a framed weblet. - Sending a user-specific token to the browser to work around the missing session.
- Doing slow or external work inside a
BEFORE_*hook.
Related topics
Evidence
| Item | Record |
|---|---|
| Guide source | Revision 5271871 plus the working-tree changes of this update |
| References | ref/ixmldoc.xml, ref/sdk/, ref/dbref.json, ref/openapi/sdk.json, LEARN.md |
| SDK inspected | zeysdk 3.3.0, source and offline help |
| Checked | Static review against the references, reading the platform and SDK source, and running the rest:resource binding examples in a local copy of the iXML engine with stand-in globals |
| Not performed | Remote compilation, deployment, and execution on an instance. Apart from the dated log entry it cites and the local engine run, this page's claims have not been executed |
Open runtime checks are listed in contributing/REVIEW_BACKLOG.md.