Agency-Watch Custom Apps
Now with AgencyOS27: a free update for everyone who already owns the watch.

⌚ Build your own app for the Agency-Watch
The watch is open. A server owner can add an app that looks and behaves like a built-in one, using the same building blocks, the same colours and the same translations.
Requirement: the Pro edition. Custom apps exist only when the Agency-Phone runs on the server and the player has it. Without Pro they are registered but never delivered: the interface never learns about them, they do not appear in the App Store, and the server refuses every action they send. That is checked in three separate places and cannot be switched off.
| File | Required | What it does |
|---|---|---|
apps/<id>/app.lua | yes | Registers the app: name, icon, colour, category. |
apps/<id>/app.js | yes | The screen: what it draws and how it reacts. |
apps/<id>/app.css | no | Your own styling. Loaded only when style = true. |
apps/<id>/server.lua | no | Server logic your app can call. |
The folder is named exactly like the app id. That is how the watch finds app.js and app.css without the path being written down twice, and paths written twice drift apart sooner or later.
Everything under apps/ is escrow_ignored: it stays readable and editable on your disk, and a watch update never overwrites it.
1 Register the app
apps/hello/app.lua
AgencyWatchApp({
id = 'hello', -- lowercase, digits and _, must match the folder
name = 'Hello',
icon = 'raster', -- one of the built-in icons, see below
color = '#5ac8fa', -- #rrggbb
category = 'tools', -- tools | games | driving | style | connected
style = true, -- also load app.css
texts = { en = 'Hello', de = 'Hallo' }, -- optional, name per language
})
An app with a malformed id, no name, an unknown category or a colour that is not #rrggbb is rejected, loudly and with a reason, in the server console. Half-registered does not exist: that kind of error only shows up when a player taps it.
Run refresh and restart Agency-Watch after adding files. On start the watch reports what registered.
2 Draw the screen
apps/hello/app.js
AgencyWatch.app('hello', {
render: function (w) { // required, returns HTML
var d = w.data();
return w.card('Hello, ' + w.esc(d.name)) +
w.section('Steps') +
w.row({ icon: 'schritte', static: true,
top: String(d.steps || 0) }) +
w.button('Roll a die', ' data-roll');
},
bind: function (w) { // optional, runs right after
w.click('[data-roll]', function () {
w.toServer('roll', { sides: 6 }).then(function (a) {
if (a.ok) w.notify('Hello', 'You rolled ' + a.data.value);
});
});
},
});
render runs every time the screen is built, so it keeps no state between calls. Anything that must survive a redraw goes in a variable outside the app object.
Both functions run inside a safety net. If your code throws, your app shows an error line and the watch keeps running; the actual error goes to the F8 console.
3 The toolbox
w holds everything an app on a watch needs, and nothing beyond it. An interface that can do everything can never be changed again, so this one is deliberately small.
| Call | What it gives you |
|---|---|
w.data() | A flat snapshot: time, hour, minute, date, weekday, name, job, health, armour, pulse, steps, distance, calories, speed, heading, weather, pro. |
w.row({icon, top, bottom, end, attrs, static}) | A list row in the watch's own style. |
w.section(text) | A section heading. |
w.card(html) | A surface. |
w.button(text, attrs) | A button. attrs is a raw attribute string, e.g. ' data-roll'. |
w.toggle(on) | An on/off switch. |
w.center(html) | Centred, for a single large value. |
w.icon(name) | One of the built-in icons. |
w.esc(text) | Escape text. Use it for everything that is not yours. |
w.text(key, fallback) | A translation from the watch. |
w.click(selector, fn) | Attach a listener. Only inside bind. |
w.notify(title, text, icon, colour) | A notification on the watch. |
w.refresh() | Redraw, without the entrance animation. |
w.close() | Go back, like the arrow in the header. |
w.toServer(action, data) | Ask the server. Returns a promise. |
w.id, w.version | Your app id and the AgencyOS version. |
The snapshot is a copy, not a handle. The watch also carries contacts, messages and card data; none of that is in there.
4 The server side
apps/hello/server.lua
AgencyWatchAction('hello', 'roll', function(src, data)
local sides = tonumber(data and data.sides) or 6
if sides < 2 then sides = 2 end
if sides > 100 then sides = 100 end
return { value = math.random(1, math.floor(sides)) }
end)
w.toServer always resolves, with { ok: true, data: … } or { ok: false, reason: '…' }, even when the server never answered. Your app can never hang waiting, and the reason always sits at the top level, no matter which side refused.
reason | Meaning |
|---|---|
no_pro | No Pro edition. Custom apps do not exist without it. |
unknown | No such app or no such action registered. |
too_fast | Rate limit: at most ten requests per five seconds per player. |
error | Your handler threw. The details are in the server console. |
timeout | No answer within twelve seconds. |
data | The request was not a table. |
Whatever arrives in data comes from the client and is a wish, not a fact. Check every value before you use it. Anything that counts (money, items, outcomes) belongs on the server and nowhere else.
5 Styling
The watch's colours are available as CSS variables and follow whatever the wearer picked in Studio. Hard-coded colours give you an app that stays blue on a green watch.
var(--text) var(--text-2) var(--leise)
var(--akzent) var(--gruen) var(--rot) var(--gelb)
var(--karte) var(--karte-hoch) var(--linie)
var(--grund)
Prefix your own classes. The screen is shared with every other app, and a class called .large will find friends.
6 Built-in icons
Pass any of these to icon in app.lua or to w.icon():
akku auto blitz brief einkauf farben flugzeug gewitter glocke haken helligkeit herz hoch hupe karte karte-pin kein-signal kino kompass kreuz krone lampe laufen lautlos links loeschen minus mobil mond nebel neuladen pause personen pin plus qr raster rechts regen regler runter sanduhr schild schloss schloss-auf schnee schritte signal sonne sp-fall sp-flug sp-ring sp-turm sprechen stoppuhr telefon telefon-aus tempo ton uhr verboten wiedergabe wlan wolke zahnrad
The icon names stay as they are on purpose. They are ids, not labels: a player never reads them, and renaming an id breaks every app that already uses it.
📦 A working example ships with the watch
apps/example/ inside the resource is a complete, runnable app: building blocks, a server call, a notification and its own CSS. Copy the folder, rename it, and you have a starting point that already works.
A short version of this page is in apps/README.md next to it.