app2dat Automation Manual

Automation Manual

Automation with app2dat

From your first script to fully automated club administration: this manual introduces the automation system step by step, serves as the complete reference for the a2d scripting API, and walks through large practical examples that automate real club work.

Updated: July 2026 For admins & automators WebApp · Android · iOS · Windows

Introduction

Recurring administrative work — exporting lists, generating certificates, transferring data between groups, sending notifications — never has to be done by hand in app2dat. The automation system runs small JavaScript programs (scripts) that access your club data through the documented a2d interface: members, data fields, sessions, attendance, chats, documents and more.

This manual is structured like a textbook: chapters 24 introduce the scripting world with simple examples. Chapters 59 form the complete reference of the a2d API. They are followed by a chapter on event triggers, three in-depth practical examples from real clubs — and, to conclude, working with the AI automation assistant that writes the scripts for you on request.

Not a programmer? No problem. You can have every script written by the built-in automation assistant — just describe in plain language what should happen. This manual still pays off: you will understand what the scripts do, verify results, and make small adjustments yourself.

Where automations live: the four levels

Automations exist on four levels (scopes) — each with its own automation manager and its own magic-wand menu :

LevelTypical scriptsContext
UserPersonal helpers, tools that work across profiles.Only userId
ProfileOrganisation-wide jobs: creating structures, working across several groups.profileId
GroupThe standard case: member lists, data fields, exports, certificates of this group. Event groups (type E) are groups too — their session and attendance scripts belong here.profileId + groupId
Event (folder)Scripts of an event folder — e.g. creating the folder’s event groups automatically or evaluating across the folder.profileId + eventId

The scope determines which context a script knows automatically (chapter 3) and how far saved variables reach (chapter 9). Most examples in this manual are group scripts — they run inside the group whose data they work on.

Safety: sandbox and permissions

  • Server-side sandbox. Scripts run isolated on the server (Jint engine) — separate from the base software, with hard limits: 50 MB memory, 100,000 statements, limited recursion depth. A faulty script cannot damage the app.
  • Your permissions, never more. Every a2d method checks the permissions of the executing user: reading requires at least member, writing at least manager/admin. A script only sees and changes what you could see and change in the app yourself.
  • Everything is logged. Every run produces an execution report with log, result and error messages.

Your first script

The best way in is a script that changes nothing — it only greets you. Along the way you learn the complete working cycle: create, write, run, read the report.

Opening the automation manager

  1. Open a group in which you are an admin and switch to Administration.
  2. Open the Automation manager via the Automation menu item. On the left you see the tree view (scripts and folders), on the right the code editor.
  3. Create a script with New script and give it a meaningful name, e.g. Greeting.

Magic wand vs. manager: The magic-wand menu only runs existing scripts — depending on the release settings also for non-managers (chapter 14). The automation manager for creating and editing is reserved for the management: admin and manager, on all four levels.

For groups two equivalent routes lead there: the manager icon in the header bar of the group administration, or the group's ⋮ menu in the group list ("Automation manager").

Hello app2dat

Type (or paste) this code into the editor and save:

// My first script: greets the executing user with their profile name.
log("Script started");

const profile = await a2d.profile.get(a2d.context.profileId);
await a2d.ui.showMessage("Hello", "Great to see you, " + profile.firstName + "!");

result.set("message", "Greeting shown for " + profile.firstName);

Run the script with the Execute button in the editor. Three things happen that you will find in every script:

  • log(…) writes a line into the execution log — your most important debugging tool.
  • await a2d.… calls the scripting API. Almost all a2d methods are asynchronous — the await in front is mandatory (chapter 3).
  • result.set("message", …) hands over the result of the run — it appears in the execution report.

The execution report

After every run the editor shows the execution report with three areas: Log (all log lines), Result (what result handed over) and Error (JavaScript error message including line number, if something went wrong). The server keeps the last five runs per level — the history icon brings them back at any time, including scripts that ran in the background via event triggers.

Be brave: As long as a script only reads (list, get, exports), nothing can break. Writing methods (set, create, delete) are easy to spot in the reference — and even they can never do more than your own role allows.

Script fundamentals

Scripts are plain, modern JavaScript. If you have programmed before you will feel at home immediately — but the scripting environment has a few peculiarities you should know. This chapter is the foundation for everything that follows.

The execution context: a2d.context

Every script knows where it runs. The (read-only) object a2d.context provides:

PropertyTypeMeaning
userIdnumberID of the signed-in user.
profileIdnumberActive profile.
groupIdnumberCurrent group (0 = no group context).
eventIdnumberCurrent event folder (0 = no event context).
scopestring"User", "Profile", "Group" or "Event".
culturestringLanguage/region of the frontend (used by a2d.date.format).

A group script almost always starts with this safeguard:

const groupId = a2d.context.groupId;
if (!groupId) { result.set("message", "Please run this script inside a group."); return; }

No function wrapper needed: The script already is the “function” — a top-level return; ends it cleanly. Do not wrap the code in an IIFE yourself ((async () => { … })()) — it is unnecessary and, depending on the exact form, can swallow the result.

await — always, but one at a time

Almost all a2d methods are asynchronous and need an await. Only a2d.vars.set/get/getAll, a2d.member.getSelected() and a2d.date.* are synchronous.

// WRONG — parallel a2d calls are not allowed in the sandbox:
// const [a, b] = await Promise.all([a2d.group.get(1), a2d.group.get(2)]);

// RIGHT — run the calls one after the other:
const a = await a2d.group.get(1);
const b = await a2d.group.get(2);

The sandbox works with a single engine instance: Promise.all/race over a2d calls cause runtime errors — the static check step of the AI assistant reports this pattern as an error.

Log, result, parameters

ObjectPurpose
log(text)Write a line into the execution log (console.log/warn/error are aliases).
result.set(key, value)Hand over a result value. A single result.set("message", …) arrives at a calling script as a plain string.
result.setJson(jsonString)Hand over a structured result as readable JSON — a caller can access properties directly.
params.get(key)Read input parameters when the script was started by another script (chapter 9) or an event trigger (chapter 10). All values are strings.
// ALWAYS output structured results via setJson:
result.setJson(JSON.stringify({ count: 3, names: ["Anna", "Ben", "Cem"] }, null, 2));

// NOT: result.set("x", object)            → appears as "[object Object]"
// NOT: result.set("x", JSON.stringify(…)) → double-escaped and hard to read

Types & formats — the three golden rules

  1. Data-field IDs are strings. Even when they look numeric ("1", "17"): fields are always addressed via their string ID. a2d.memberData.getFieldId(groupId, "Field name") resolves the ID from the name.
  2. Dates are ISO. The canonical format is yyyy-MM-dd. Dialog inputs may arrive in local format — normalise with a2d.date.toIso(value) before saving or passing on; use a2d.date.format(value) for display.
  3. Dialog and parameter values are strings. showInput returns strings even for number and bool fields — convert yourself: parseInt(res.x, 10), res.flag === "1". The same applies to everything from params.get(…).

Handling errors

If an a2d method throws an error (e.g. missing permission), the script aborts and the execution report shows the message including the line number. Where a single failure must not stop the run — for example in a loop over many members — use try/catch:

for (const pid of members) {
    try {
        await a2d.memberData.set(groupId, pid, fieldId, "OK");
    } catch (e) {
        log("Skipped (profile " + pid + "): " + e.message);
    }
}

Be careful with result objects coming from the backend (e.g. from attendance.profilesAttendances): they behave like .NET dictionaries — accessing a key that does not exist can throw. Access defensively (try/catch or existence check).

Executing in the editor

The Execute button in the editor runs the script for real — with real data and real writes; there is no separate dry run. The only special case: if the script uses a2d.member.getSelected(), the editor asks for the member selection in a dialog beforehand — when started from the member list, its current selection is used instead. So approach new scripts step by step: build them read-only first (list/get + result), check the execution report — and only then add writes, using a small member selection for the first real run.

Sandbox limits: 50 MB memory, 100,000 statements, recursion depth 100. A script waits 5 minutes for an open dialog (showInput, confirm, showSelect) or 10 minutes for pickFile — after that the call aborts. More than enough for club data — endless loops are stopped reliably by the statement limit.

Members, data & dialogs

Time to get practical: the three most common ingredients of almost every club script are the member list, the group’s data fields and dialogs for input and output. We assemble them step by step.

Reading and filtering members

a2d.member.list(groupId) returns all members of the group — each with profileId, status, the embedded profile object (first name, last name, …) and the data fields in data. The following script lists all inactive members (status not equal to "ACT") in a readable form:

// Lists members of the current group filtered by status.
const groupId = a2d.context.groupId;
if (!groupId) { result.set("message", "Please run this script inside a group."); return; }

const members = await a2d.member.list(groupId);

const inactive = (members || [])
    .filter(m => (m.status || "") !== "ACT")
    .map(m => {
        const fn = m.profile ? (m.profile.firstName || "") : "";
        const sn = m.profile ? (m.profile.secondName || "") : "";
        return {
            profileId: m.profileId,
            name: (fn + " " + sn).trim() || ("Profile " + m.profileId),
            status: m.status
        };
    });

log("Inactive members: " + inactive.length);
result.setJson(JSON.stringify({ count: inactive.length, members: inactive }, null, 2));

There is no m.name: The display name is always assembled from profile.firstName and profile.secondName. Status codes: ACT active, INV invited, REQ requested, DEL removed — the complete list is in chapter 5.

Using the member-list selection

Many scripts should not affect all members but the ones the user has selected in the member list. That is exactly what a2d.member.getSelected() returns — synchronously, as an array of profile IDs:

const selected = a2d.member.getSelected() || [];
if (selected.length === 0) {
    await a2d.ui.showMessage("Note", "Please select members in the list first.");
    return;
}
for (const pid of selected) {
    // pid is a number (profile ID), not an object
}

Reading and writing data fields

The custom columns of a group (see Administration Manual, chapter 8) are accessed via a2d.memberData. Fields are addressed via their string ID; you resolve the ID once from the field name:

const noteId = await a2d.memberData.getFieldId(groupId, "Note");
if (!noteId) { result.set("message", "Data field 'Note' not found."); return; }

// Read: all field values of one member as { fieldId: value }
const data = (await a2d.memberData.get(groupId, pid)) || {};
log("Current note: " + (data[noteId] || "—"));

// Write: set a single field (manager role or higher)
await a2d.memberData.set(groupId, pid, noteId, "Fee 2026 paid");

After writing changes it pays to end the script with await a2d.ui.refresh(); — the visible member list updates immediately, without the user reloading the view.

Dialogs: ask instead of hard-coding

Through a2d.ui your script talks to the user. The following example combines an input dialog with a member search — note the pattern: one dialog, whose result is read via res[id], and a null check for “cancel”:

// Searches members via an input dialog (first or last name).
const res = await a2d.ui.showInput("Search members", [
    { id: "q", name: "First or last name", type: "string", required: false }
]);
if (!res) return; // Cancelled by the user

const term = ((res && res.q) || "").toString().trim().toLowerCase();

const members = await a2d.member.list(a2d.context.groupId);
const matches = (members || []).filter(m => {
    const fn = (m.profile && m.profile.firstName ? m.profile.firstName : "").toLowerCase();
    const sn = (m.profile && m.profile.secondName ? m.profile.secondName : "").toLowerCase();
    return term === "" || fn.includes(term) || sn.includes(term);
});

log("Matches: " + matches.length);
result.setJson(JSON.stringify({ term: term, count: matches.length }, null, 2));

Besides showInput there are showMessage (OK message), confirm (yes/no), showSelect (choice list) and pickFile (choose a file — returns the file’s content). All details and field types are in chapter 9.

You can already do a lot with this: filter members, write values, ask questions, output results — that is the skeleton of most automations. The next five chapters are your reference for everything else.

Reference: profiles, groups & members

From here on the manual becomes a reference. Conventions: all methods are asynchronous (await!) unless explicitly marked “synchronous”. Reading requires at least the member role, writing at least manager/admin — methods that go beyond this are marked. Optional parameters carry a ?.

a2d.profile — profiles

MethodDescription
get(profileId)Load a profile: id, type ("PRS"/"ORG"), firstName, secondName, title, email, phoneNumber, dateOfBirth, gender, idNumber, imageUrl.
getInfo(profileId)Short info — lighter than get when only name/picture are needed.
getField(profileId, field)A single profile field as string (or null).
list()All profiles of the signed-in user.
create(data)New managed profile. firstName is required (for type:"ORG" = organisation name); further fields: type (default "PRS"), secondName, title, email, phoneNumber, dateOfBirth (ISO), gender, idNumber, imageUrl, templateGroupId?/pin? (ORG only: fill the member group from a template).
edit(profileId, data)Change profile fields — every key in data is a field name.

ORG profiles bring their member group along: After profile.create({ type: "ORG", … }) the member system group already exists — fetch it with a2d.group.getMemberGroup(profileId). Note that the privacy-resolved date of birth of a member is only returned by a2d.member.get(groupId, pid), not by profile.get.

a2d.group — groups

MethodDescription
get(groupId)Load a group: id, profileId, name, description, type ("P" standard / "E" event group / "M" member system group), access ("PRV"/"MMB"/"PUB"), childrenType ("PRS"/"ORG"), membVisible.
findByName(name)Find a group by name — first in the current profile, then in the others; the first match wins. Important because system groups like “Members” exist in every ORG profile.
list(profileId)All groups of a profile.
create(data)New group. name required; optional description, type (default "P"; with eventId automatically "E"), access ("PUB" only for verified organisations), eventId (assigns the group to an event folder), childrenType, profileId (default: context).
createFromTemplate(data)New group from a template group: adopts data-field definitions including visibilities, template files, the automation folder structure (scripts as live references) and topic definitions (without messages). In addition to the create fields: templateGroupId (required), pin? (for a foreign, PIN-protected template).
getMemberGroup(profileId)Member system group (type "M") of an ORG profile.
edit(groupId, data)Change a group (manager+): name, description, access — only the fields you pass.
getMembers(groupId)All members including profile data (like member.list).
getMemberIds(groupId)Only the profile IDs.
delete(groupId)Delete the group including data, files and sessions — the only method that requires the admin role (manager is not enough); only types "P"/"E".

a2d.event — event folders

Event folders group event groups at profile level (see Administration Manual, chapter 6). The groups themselves are created with group.create({ …, eventId }).

MethodDescription
list(profileId)All event folders of a profile.
get(eventId)Load a single event folder.
create(data)New event folder: name required; description, profileId (default: context), imageUrl?.
edit(eventId, data)Change: name, description.

a2d.member — members

MethodDescription
list(groupId)All members with profileId, status, statusDate, embedded profile object and the data fields in data.
listActive(groupId)Like list, but only active members (status "ACT" or "RNAC") — invited, requested and removed ones are excluded.
get(groupId, profileId)A single member with all data — including the privacy-resolved profile.dateOfBirth.
getSelected()Synchronous. The members selected in the app as an array of profile-ID numbers (null when nothing is selected).
inviteMember(groupId, profileId)Send an invitation — the profile has to accept.
addMember(groupId, profileId)Add directly without invitation (manager+).
setStatus(groupId, profileId, status)Set the status (manager+), e.g. "ACT", "NAC", "DEL" — use only codes that really exist (there is no status "INA").
getStatus(groupId, profileId)Query the current status (or null).
remove(groupId, profileId)Remove the member (status becomes "DEL").
count(groupId, status)Number of members with a given status.

Important status codes: ACT active · RNAC registered member (counts as active) · INV invited · REQ join requested · RACT reactivation requested · NAC signed off/inactive · DEL removed.

a2d.memberData — data fields

MethodDescription
get(groupId, profileId)All field values of a member as { fieldId: value }.
set(groupId, profileId, fieldId, value)Set a single field.
setMultiple(groupId, profileId, data)Several fields at once: data = { fieldId: value }. Dates in ISO; for list fields store the key, not the display text.
getDataFields(groupId)Field definitions: { id, name, type, list? }. List fields have list = array of { key, text }.
getFieldId(groupId, name)Resolve the field ID from the field name (cached; null if missing).
createField(groupId, options)Create a new data field (manager+); returns the field including id. options: name (required), type (required), access (default "-"), visible (default true = column in the member list), description, section.
copy(sourceGroupId, sourceProfileId, targetGroupId, fieldMapping)Copy field values between groups; fieldMapping = { sourceField: targetField }.
delete(groupId, profileId, fieldIds)Clear specific fields of a member (fieldIds = array).

Field types for createField: string, number (integer), decimal, date, time, list, bool, pay (payment), doc (document), grmmb (member link). Common aliases like text, int, select, checkbox, payment, file, members are translated automatically.

Visibility codes (access) — what members see of the field: "-" invisible (managers only) · "MR" visible but not editable · "MW" editable by the member.

Reference: sessions & attendance

a2d.session — sessions

MethodDescription
list(groupId)All sessions of the group.
get(groupId, sessionId)Load a single session.
create(groupId, data)Create a session (manager+). Required: type ("S" single / "W" weekly recurring), start ("yyyy-MM-dd HH:mm"; alias date for all-day), end, locationId (a location of the profile). Optional: name.
edit(groupId, sessionId, data)Change a session: type, start, end, name. The location (locationId) and the date alias are not supported here — a location change is not possible via script.
delete(groupId, sessionId)Delete a session.

Location asymmetry: list/get return the location as a nested object — read the ID with s.location?.id (s.locationId is undefined!). When creating, however, you pass the flat field locationId.

a2d.attendance — attendance

MethodDescription
get(groupId, sessionId, date)Sign-offs for a date: { profileId: "note" }.
signOff(groupId, profileId, sessionId, date, note)Sign a member off a session.
signOn(groupId, profileId, sessionId, date)Withdraw a sign-off.
getStats(groupId, sessionId, fromDate, toDate)Statistics: totalDates (number of dates) and absences = { profileId: count }.
profilesAttendances(profileIds, groupId, startDate, endDate, option)Attendance matrix of several profiles (manager+). Returns { profileId: { "yyyy-MM-dd HH:mm": "1" | "0" } } — per profile a session→status dictionary ("1" present, "0" signed off, missing key = not recorded; the key is the session start including time). option: null = normal attendance; "_R" = only where the member was responsible and present. The special entry [-1] has every session of the period as keys (same format) — ideal as a column list for matrices.

Date strictness: For get/signOff/signOn the date becomes part of the storage name — only real date values (yyyy-MM-dd recommended) are accepted; anything else aborts with “invalid date”. Signing yourself off requires an active membership; other members require manager rights. Read results defensively: att[pid] || {}, access inside try/catch (chapter 3).

Reference: communication & tasks

a2d.topic — topics/chats

MethodDescription
list(groupId)All topics of the group.
get(groupId, topicId)Load a single topic.
create(groupId, data)Create a topic (manager+). title required (alias description); comments (default false: true = replies allowed, false = announcement only), emojis (default ["👍","👎"]; array or ;-string; empty = no reactions), members (default all; "all"/"none" or profile-ID array).
edit(groupId, topicId, data)Change: title, comments.
delete(groupId, topicId)Delete a topic.
getMembers(groupId, topicId)Participants as profile IDs ([-1] = all group members).

a2d.workTask — tasks

MethodDescription
list(groupId)All tasks of the group.
get(groupId, taskId)Load a single task.
create(groupId, data)Create a task (manager+). title (alias description), priority ("0" none / "1" low / "2" medium / "3" high / "4" highest), byDate (ISO), byTime (HH:mm), status, comments, spentTime.
edit(groupId, taskId, data)Change: title/description, priority, byDate, byTime, status, spentTimecomments can only be set on creation. A status change automatically updates the change date.
delete(groupId, taskId)Delete a task.
getMembers(groupId, taskId)Assigned profiles.

a2d.chat — messages

MethodDescription
sendMessage(groupId, topicId, topicType, text)Send a message into a topic (topicType "T") or a task ("W").
getMessages(groupId, topicId, topicType, limit)Read messages (default limit 50).
sendPrivateMessage(targetProfileId, text, fromProfileId?)Private 1:1 message. fromProfileId = sender profile (must be one of the executing user’s own profiles); in event scripts pass event.actorProfileId. Allowed towards profiles that share a group with the sender, or — when managing a group — towards its active members.

a2d.notification — push notifications

MethodDescription
sendPush(targetProfileId, title, message)Push to a single profile.
sendPushToGroup(groupId, title, message)Push to all active group members (status ACT/RNAC); returns the number of messages sent.

Rate limit: At most 100 push messages and 50 e-mails per hour — plan bulk actions accordingly.

Reference: PDF, export & import

a2d.document — documents & folders

MethodDescription
getFolders(groupId)Document folders of the group.
createFolder(groupId, title)Create a new folder.
getDocuments(groupId, folderId)Documents of a folder.
deleteFolder(groupId, folderId)Delete a folder.

a2d.pdf — PDF generation

PDFs are always built in the same order: prepare()addPage()addText()/addImage()close…(). All measurements are millimetres; an A4 sheet is 210 × 297.

MethodDescription
prepare(profileId, groupId, title, subject, width, height, templateLink, memberProfileId, templatePin?)Start a PDF (manager+). memberProfileId > 0 = the finished PDF is linked to that member; 0 or -1 = pure download PDF. templateLink = background PDF (empty string = no template).
addPage()New page — call before the first text/image.
addText(textId, text, fontFamily, size_mm, bold, italic, color, x_mm, y_mm, centered)Place text. Positional parameters, no options object. Baseline at y_mm; centered = horizontally centred at x_mm; color = [r,g,b] (empty array = default).
addImage(x_mm, y_mm, scale, path, pin?)Place a PNG/JPG (positional). scale 1.0 = pixel size.
close()Close without saving.
closeAndDownload(fileName)Save and offer for download (only when memberProfileId was 0/-1).
closeAndLinkToMember(fieldId)Save and link to the member under data field fieldId (only when memberProfileId > 0); returns the link path.

File access is protected: templateLink and the addImage path expect a complete app2dat file link (as stored in a document data field) — free-form paths or typed-in texts do not work. Files of foreign organisations are only reachable with a template release and PIN; use the placeholder %GPIN% instead of a hard-coded PIN — it stays dynamic in referenced scripts (chapter 14).

a2d.export — exports & downloads

MethodDescription
membersCSV(groupId, fieldIds, options)Members as a CSV string (manager+). List/member fields are resolved into display texts, dates localised. fieldIds: array of column IDs or null = standard columns; additionally allowed: "Name", "Status", "StatusDate", "Respons", "ParentId", "Profile.<Field>" (e.g. "Profile.Email"), "Profile.Location.<Field>".
profileImages(groupId, profileIds, fileName, layout)Profile pictures as a label-sheet PDF, offered for download (manager+). profileIds = null = all active. All four arguments are required — pass null for the ones you do not need.
qrCodes(groupId, profileIds, fileName, layout)QR codes (encoding P<profileId>, first/last name below) as a label-sheet PDF — parameters identical to profileImages.
download(content, fileName)Offer a text file (e.g. CSV) for download.
downloadBytes(data, fileName)Offer binary data for download.

options for membersCSV: separator (default ";"), includeHeader (default true), onlyActive, profileIds (only these, in output order), headers ({ fieldId: "heading" }) and attendance for event groups: from/to (required, ISO), count (“present/total” column), sessions (one column per session), responsibility (transposed responsibility matrix), allDates.

layout for label sheets (mm; defaults = Avery-Zweckform 3652, 3 × 7 labels of 70 × 42.3 on A4): pageWidth, pageHeight, width, height, cols, rows, marginLeft, marginTop, gapHorizontal, gapVertical, skip (skip labels already used).

Excel tip: Prefix your own CSV texts with a UTF-8 BOM ("\uFEFF" + csv) before handing them to download — otherwise Excel displays umlauts incorrectly.

a2d.import — CSV import

MethodDescription
membersFromCSV(groupId, csvText, mapping, options)Writes CSV values into data fields of existing members (manager+). mapping = { "CSV column": "targetFieldId" }; options: separator (";"), skipHeader (true), matchBy ("ProfilId", "Name" or "Email"). Returns { imported, errors }.
membersFromCSVMap(groupId, csvText, mapText, options)Creates new managed members from CSV + map file — like the GUI import: profile including address and categories, group join, data fields. Existing members (same first + last name) are skipped. Returns { imported, skipped, errors }.

Careful with list fields: membersFromCSV writes values raw — resolve CSV display texts into the stored list keys first via memberData.getDataFields, or unknown values end up in the field. The map format for membersFromCSVMap is described in detail in chapter 13.

Reference: dialogs, variables & tools

a2d.ui — dialogs & interface

MethodDescription
showMessage(title, text)Show an OK dialog; waits for confirmation.
confirm(title, text)Yes/no dialog; returns true/false.
showInput(title, fields)Dynamic input dialog. Returns an object with the values per field id — or null on cancel. Instance variables from a2d.vars.set() pre-fill fields whose id matches the variable name.
showSelect(title, options)Choice list (array of strings); returns the chosen option as a string (not the index), null on cancel/timeout.
pickFile(title, extensions?)Native file dialog. Returns the text content of the chosen file (not path or name), null on cancel. extensions e.g. ".csv" or ".csv,.map".
refresh()Refreshes the visible list in the app (members/groups/profiles — depending on context). Recommended as the last step of every writing script.

Field objects for showInput: id (required, except for type:"label"), name (caption), type, required (default false), description. Types (always lowercase): string · number · decimal · date · time · bool · label (plain hint text); unknown types fall back to string. All returned values are strings (chapter 3).

a2d.vars — variables

MethodDescription
set(name, value) / get(name) / getAll()Synchronous. Instance variables — live only during the current run; also serve as pre-fill for showInput.
save(name, value, scope?)Store a value persistently — across runs.
load(name, scope?)Load a persistently stored value.

Reach (scope): without = current level · "group" this group · "event" this event folder · "profile" all groups of the same profile · "user" all profiles of the user. Saving and loading must use the same scope; to pass data between groups of different profiles, use "user".

a2d.date — dates (synchronous)

MethodDescription
format(value, culture?)Short date in the culture’s format (without argument: a2d.context.culture) — for display.
toIso(value)Normalise any date notation into the canonical yyyy-MM-dd — before saving/passing on.

a2d.automation — calling scripts from scripts

MethodDescription
executeScript(scriptName, parameters)Runs another script and returns its result. scriptName is resolved in the own scope; for scripts of other groups create a local reference and call its name (the code is read live but runs in the caller’s context). Parameter values arrive in the sub-script as strings via params.get(…); the return value comes from its result. Throws on errors — use try/catch where needed.
// Caller: start a sub-script, pass parameters, use the result
const r = await a2d.automation.executeScript("Calculate fee", {
    base: "42.50",
    factor: "1.2"
});
log("Amount: " + r.amount + " " + r.currency);

// Sub-script "Calculate fee":
// const base   = parseFloat(params.get("base"));
// const factor = parseFloat(params.get("factor"));
// if (isNaN(base) || isNaN(factor)) { result.set("message", "Invalid parameters"); return; }
// result.setJson(JSON.stringify({ amount: (base * factor).toFixed(2), currency: "EUR" }));

This is how you split larger automations into reusable building blocks — for example a central fee calculation used by several scripts.

Event triggers

So far you have started scripts by hand. Event triggers turn this around: the script runs automatically as soon as something changes in the group — with no interaction at all. Event triggers are configured on group scripts — in the editor via the script’s Triggers dialog (chapter 14). In addition, the server evaluates profile-wide event scripts (profile scope), which fire for all groups of the profile.

The three event types

Eventevent.typeFires on …
Status changeMemberStatusChangedJoin, sign-out, status change of one or more members.
Data changeMemberDataChangedChange of a member’s data fields.
Attendance changeMemberAttendanceChangedSign-on/off for a session, or session cancellation.

A script can subscribe to several event types at once and branch on event.type. Important: event scripts run on behalf of the user who triggered the change — not as “system”. They can do exactly what the triggering user could do.

The event.* parameters

The script receives the event data via params.get(…) — all values as strings, lists comma-separated:

ParameterContent
event.typeEvent type (see above) — always present.
event.groupId / event.groupProfileIdGroup of the event and its organisation profile.
event.actorUserId / event.actorProfileIdTriggering user and their acting profile (self-change: the changed profile; admin action: the admin’s profile). The ideal sender for chat.sendPrivateMessage.
On status change: event.profileIds (list), event.memberCount, event.status.<profileId> (new status per member); with exactly one affected member additionally event.profileId and event.status.
On data change: event.profileId, event.changedFields (list of field keys), event.field.<key> (new value per field).
On attendance change: event.sessionId, event.date (yyyy-MM-dd), event.memberIds (list; the value -1 means: the whole session was cancelled), event.memberCount.

Example: one watchdog script for all three events

This script (from the built-in example library) subscribes to all three event types and sends a designated person a private message with the details:

// Reacts to ALL three event-trigger types and sends a private message with the relevant data.
// Set up as an event trigger and tick all three event types (group scope).
// event.actorProfileId = the acting profile (self-action: the changed profile; admin action: the admin's profile).

const NOTIFY_PROFILE_ID = 1;   // recipient of the notification (change this)

const type = params.get("event.type");
if (!type) { result.set("message", "This script is meant for an event trigger (no event.type present)."); return; }

const groupId = params.get("event.groupId");
const sender  = parseInt(params.get("event.actorProfileId") || "0", 10); // 0 = resolve sender automatically

log("Event received: " + type + " (group " + groupId + ")");

// comma-separated list -> array (empty entries removed)
function asList(csv) {
    return (csv || "").split(",").map(s => s.trim()).filter(s => s.length > 0);
}

let text;

if (type === "MemberStatusChanged") {
    // status of one/more members changed (join, unregister, set status ...)
    const ids = asList(params.get("event.profileIds"));
    const parts = ids.map(id => "profile " + id + " -> " + (params.get("event.status." + id) || "?"));
    text = "Status change in group " + groupId + ": " + parts.join(", ");

} else if (type === "MemberDataChanged") {
    // member data changed
    const profileId = params.get("event.profileId");
    const fields = asList(params.get("event.changedFields"));
    const parts = fields.map(f => f + " = " + (params.get("event.field." + f) || ""));
    text = "Data change for profile " + profileId + " in group " + groupId
         + (parts.length ? ": " + parts.join(", ") : "");

} else if (type === "MemberAttendanceChanged") {
    // attendance changed (memberId -1 = whole session cancelled)
    const date      = params.get("event.date");
    const sessionId = params.get("event.sessionId");
    const ids       = asList(params.get("event.memberIds"));
    if (ids.length === 1 && ids[0] === "-1") {
        text = "Session " + sessionId + " on " + date + " (group " + groupId + ") was cancelled.";
    } else {
        text = "Attendance change in group " + groupId + ", session " + sessionId
             + " on " + date + ": profile(s) " + ids.join(", ");
    }

} else {
    text = "Unknown event: " + type;
}

// send: sender = the trigger's acting profile, recipient = NOTIFY_PROFILE_ID
try {
    await a2d.chat.sendPrivateMessage(NOTIFY_PROFILE_ID, text, sender);
    result.set("message", "Sent to profile " + NOTIFY_PROFILE_ID + ": " + text);
} catch (e) {
    result.set("message", "Send failed: " + e.message + " | " + text);
}

Where do I see what happened? Event triggers run in the background — their reports are available in the editor via the execution history (the last five runs per level are kept).

Practice: evaluating attendance across several groups

The first large practical example comes — like the two that follow — from the curated example library that the automation assistant also draws on. The task: a club trains in several groups. For the selected members, count how often each of them was present anywhere in the queried period — and write count and percentage back into data fields of the current group.

Prerequisites

  • The current group has the data fields “Anzahl” (count) and “Prozent %” (percent).
  • The executing user manages the training groups involved (the attendance matrix is an admin function).
  • Members are selected in the list before starting.

The script

// Calculate training attendances
// For each selected member, counts the attendances across several training groups within the
// requested period and writes Present ("Anzahl") and percent ("Prozent %") into the
// current group. The percent basis (max. number of trainings) is requested in the dialog.

const groupId = a2d.context.groupId;
if (!groupId) { await a2d.ui.showMessage("Error", "Please run this script inside a group."); return; }

const selectedMembers = a2d.member.getSelected() || [];
if (selectedMembers.length === 0) {
    await a2d.ui.showMessage("Error", "No members selected");
    return;
}

const anwesendId = await a2d.memberData.getFieldId(groupId, "Anzahl");
const prozentId = await a2d.memberData.getFieldId(groupId, "Prozent %");
if (!anwesendId || !prozentId) {
    await a2d.ui.showMessage("Error", "Data fields 'Anzahl' and/or 'Prozent %' not found.");
    return;
}

// Pre-fill from persistent variables
a2d.vars.set("datFrom", await a2d.vars.load("datFrom") || new Date().toLocaleDateString("de-DE"));
a2d.vars.set("datTo", await a2d.vars.load("datTo") || new Date().toLocaleDateString("de-DE"));
a2d.vars.set("maxTrainings", await a2d.vars.load("maxTrainings") || "");

const inputFields = [
    { type: "label", name: "All fields must be filled in" },
    { type: "date", id: "datFrom", name: "Date from*", description: "" },
    { type: "date", id: "datTo", name: "Date to*", description: "" },
    { type: "number", id: "maxTrainings", name: "Max. number of trainings*",
      description: "Enter here the maximum number of possible trainings for the % calculation." }
];
const result = await a2d.ui.showInput("Evaluate attendances", inputFields);
if (!result) return; // Cancelled by user

const mt = parseInt(result["maxTrainings"], 10);
if (isNaN(mt) || mt <= 0) {
    await a2d.ui.showMessage("Error", "Please enter a valid max. number of trainings (greater than 0).");
    return;
}

await a2d.vars.save("datFrom", result["datFrom"]);
await a2d.vars.save("datTo", result["datTo"]);
await a2d.vars.save("maxTrainings", result["maxTrainings"]);

// Training groups to sum over (enter the IDs of your own groups)
const groupIds = [111, 222, 333];
const counts = {};

for (const gid of groupIds) {
    const pa = await a2d.attendance.profilesAttendances(selectedMembers, gid, result["datFrom"], result["datTo"], null);
    if (!pa) continue;

    for (const pid of selectedMembers) {
        // pa is a .NET dictionary; accessing a key that does NOT exist can
        // throw an exception → guard defensively (skip member without attendance).
        let dates;
        try { dates = pa[pid]; } catch (e) { dates = null; }
        if (!dates) continue;

        if (!counts[pid]) counts[pid] = 0;
        for (const dat of Object.keys(dates)) {
            if (dates[dat] === "1") counts[pid]++;
        }
    }
}

let written = 0;
for (const pid of Object.keys(counts)) {
    const count = counts[pid];
    await a2d.memberData.set(groupId, pid, anwesendId, count.toString());
    await a2d.memberData.set(groupId, pid, prozentId, (count / mt * 100).toFixed(1));
    written++;
}

// Refresh the visible member list so the new values appear immediately
await a2d.ui.refresh();

await a2d.ui.showMessage("app2dat", written + " members updated.");

What you learn here

  1. A dialog with memory. The pattern a2d.vars.set(id, await a2d.vars.load(id) || default) before showInput pre-fills the fields with the values of the previous run — after the dialog they are remembered again with vars.save. The user types period and maximum only once.
  2. Validate inputs. showInput returns strings — parseInt + an isNaN check prevent division by zero or nonsense in the percentages.
  3. The attendance matrix. profilesAttendances returns a session→status dictionary per profile (keys "yyyy-MM-dd HH:mm"); every entry with value "1" is counted. The loop over several groupIds sums across groups — and the try/catch protects against the .NET dictionary behaviour on missing keys.
  4. Write back + refresh. Two memberData.set calls per member, then ui.refresh() — the new column values appear in the list immediately.

Variation: If the groups should not be hard-coded, ask for them via showInput or store them with vars.save in the profile scope — or let the assistant adapt the script to your group names.

Practice: certificates as PDF per member

After the belt exam, every successful member should receive their certificate — as a PDF, automatically stored with the member. This example generates one separate PDF per selected member and links it to the member’s document data field “Urkunde” (certificate). It demonstrates the complete a2d.pdf craft: prepare, add a page, place texts with millimetre precision, insert a stamp image, link.

Prerequisites

  • Data fields “Urkunde” (type document) and “Kyu” (grade) in the current group.
  • A stamp/signature image stored as a file of the group — you enter its complete file link in the script (placeholder in the example).
  • Select members in the list, then run.

The script

// Create certificates (for selected members)
// Generates ONE PDF per member and links it to the member's data field "Urkunde".

const groupId = a2d.context.groupId;
const profileId = a2d.context.profileId;
if (!groupId) { await a2d.ui.showMessage("Error", "Please run this script inside a group."); return; }

const selectedMembers = a2d.member.getSelected() || [];
if (selectedMembers.length === 0) {
    await a2d.ui.showMessage("Error", "No members selected");
    return;
}

// === Exam data (pre-filled from persistent variables) ===
a2d.vars.set("pruefer", await a2d.vars.load("pruefer") || "Max Mustermann");
a2d.vars.set("ort", await a2d.vars.load("ort") || "Musterstadt");
a2d.vars.set("dat", await a2d.vars.load("dat") || new Date().toLocaleDateString("de-DE"));

const inputFields = [
    { type: "label", name: "Fields marked with * are mandatory" },
    { type: "label", name: "Important! Set the scaling to 100% when printing!" },
    { type: "string", id: "pruefer", name: "Examiner*", description: "Enter only the chairman of the examination board here" },
    { type: "string", id: "ort", name: "Place*", description: "" },
    { type: "date", id: "dat", name: "Date*", description: "" }
];
const result = await a2d.ui.showInput("Enter exam data", inputFields);
if (!result) return; // Cancelled by user

await a2d.vars.save("pruefer", result["pruefer"]);
await a2d.vars.save("ort", result["ort"]);
await a2d.vars.save("dat", result["dat"]);

const datFormatted = new Date(result["dat"])
    .toLocaleDateString("de-DE", { day: "numeric", month: "long", year: "numeric" })
    .replace(/(\d+)\s/, "$1. ")
    .replace(",", "");
// "6 März, 2026" → "6. März 2026"

const urkId = await a2d.memberData.getFieldId(groupId, "Urkunde");
const kyuId = await a2d.memberData.getFieldId(groupId, "Kyu");
if (!urkId || !kyuId) { await a2d.ui.showMessage("Error", "Data field 'Urkunde' and/or 'Kyu' not found."); return; }

for (const pid of selectedMembers) {
    const memberprofile = await a2d.profile.get(pid);
    if (!memberprofile) continue;
    const data = (await a2d.memberData.get(groupId, pid)) || {};

    await a2d.pdf.prepare(profileId, groupId, "Kyu-Urkunde", "Kyu-Prüfung", 210, 297, "", pid);
    await a2d.pdf.addPage();

    const fullName = (memberprofile.firstName || "") + " " + (memberprofile.secondName || "").toUpperCase();
    await a2d.pdf.addText("big7", fullName, "Arial", 7, true, false, [], 105, 135, true);

    const kyu = data[kyuId] || "?";
    await a2d.pdf.addText("big9", kyu + ". Kyu", "Arial", 9, true, false, [], 105, 172, true);

    await a2d.pdf.addText("smallitalic", result["pruefer"], "Arial", 2.7, true, false, [], 40, 277, true);
    await a2d.pdf.addText("smallbold", result["ort"], "Arial", 2.7, true, false, [], 105, 277, true);
    await a2d.pdf.addText("smallbold", datFormatted, "Arial", 2.7, true, false, [], 105, 281, true);

    // Insert the complete app2dat file link of the stamp image (e.g. copied from a document data field)
    await a2d.pdf.addImage(60, 247, 0.2, "https://api.app2dat.com|0/0/0/2/groups/GROUP_ID/stamp.png¦Stamp_Signature.png");
    await a2d.pdf.CloseAndLinkToMember(urkId);
}

// Refresh the visible member list (linked certificates appear immediately)
await a2d.ui.refresh();

await a2d.ui.showMessage("app2dat", selectedMembers.length.toString() + " certificates created");

What you learn here

  1. One PDF per member. The decisive trick is in pdf.prepare(…, pid): the last parameter links the PDF to exactly this member — which is why the whole prepare-→-close cycle sits inside the loop. CloseAndLinkToMember(urkId) stores the finished certificate directly in the member’s document field.
  2. Millimetres instead of pixels. All positions are mm on the A4 sheet (210 × 297): addText(…, 105, 135, true) centres the name horizontally at x = 105 mm — the middle of the page. That way the layout fits pre-printed certificate sheets.
  3. Embedding field values. The Kyu grade comes from the member’s data field via data[kyuId]; profile data (name) from profile.get. Examiner, place and date are asked in the dialog — again with the vars memory from chapter 11.
  4. Images via file links. addImage requires the complete app2dat file link including the display name (after the ¦ character). You get such a link e.g. from a document data field — the sandbox does not accept free-form paths (chapter 8).

Everything in one PDF? If all certificates should end up in a single downloadable file (one page per member, optionally with a background template), call prepare only once with memberProfileId = -1 and finish with closeAndDownload — exactly what the sister example certificate export from the example library does.

Practice: importing new members from CSV

When switching to app2dat, the member list usually already exists — as an Excel or CSV file. This example creates new managed members from it: it opens the native file dialog for the CSV and a mapping file (.map), creates profiles including address and categories, joins the group and fills the data fields — duplicates are skipped.

The map file: assigning columns

The .map file is a simple text file following the pattern TargetField=CSV column, one line per assignment:

Profile.FirstName=FirstName
Profile.SecondName=LastName
Profile.Gender[list:M|male:W|female:D|diverse]=Gender
Profile.DateOfBirth[date]=DOB
Profile.Location.City=City
Profile.Categories=martialarts|karate
Data.Graduierung=Grade
Target fieldMeaning
Profile.<Field>Profile field of the new member (FirstName, SecondName, DateOfBirth, Gender, …).
Profile.Location.<Field>Address field (Street, City, ZipCode, …).
Data.<FieldName>Group data field — addressed via the field name.
[date]Modifier: interpret the CSV value as a date.
[list:key|text:…]Modifier: translate CSV display texts into stored list keys.
Field=constant without CSV column / [value]Constant value for all rows (e.g. Profile.Categories).

The script

// Import members from CSV (create new profiles)
// Selects a CSV and a MAP file via the native file dialog and creates new members from them —
// like the GUI import "Import CSV". Run this in the members group (main group).

const groupId = a2d.context.groupId;
if (!groupId) { await a2d.ui.showMessage("Import", "Please run this script in the members group."); return; }

// Select CSV file (the file content is returned as text)
const csv = await a2d.ui.pickFile("Select CSV file", ".csv");
if (!csv) return; // Cancelled

// Select MAP file
const map = await a2d.ui.pickFile("Select MAP file", ".map");
if (!map) return; // Cancelled

// Import: creates new members; existing ones (first + last name) are skipped.
const res = await a2d.import.membersFromCSVMap(groupId, csv, map, { separator: ";" });

let msg = res.imported + " new members created";
if (res.skipped > 0) msg += ", " + res.skipped + " skipped (already exist)";
if (res.errors && res.errors.length > 0) msg += ", " + res.errors.length + " errors";

// Refresh the visible member list so the new members appear immediately
await a2d.ui.refresh();

await a2d.ui.showMessage("CSV import", msg);
result.set("message", msg);

What you learn here

  1. pickFile returns content, not a path. The return value is the text of the chosen file — it goes straight into membersFromCSVMap. Check for null twice: the user can cancel either dialog.
  2. One method, the whole import. membersFromCSVMap handles profile creation, address, categories, group join and data fields in one go — and deduplicates by first + last name. The result object (imported/skipped/errors) becomes the success message.
  3. Report the result twice. showMessage informs the user immediately; result.set("message", …) keeps the same summary in the execution report.

Only updating data? If no new members should be created but fields of existing members should be filled from a table, a2d.import.membersFromCSV with matchBy "Name"/"Email" is the right tool (chapter 8) — for list fields, resolve display texts into list keys first.

The automation assistant

You now know the whole API — but you do not have to memorise it. The automation assistant is an app2dat expert that writes, modifies and explains scripts for you. This chapter shows how to work with it — and the toolkit around it: scopes, folder structure, triggers and version management in the editor.

How the assistant works

You open the assistant in the automation manager via the AI icon — it appears as a chat panel next to the editor. Describe in plain language what should happen:

  1. It looks around. The assistant knows your context (level, group, selected members) and inspects things on its own: existing scripts and folders, your groups, their data fields and members — exactly as far as you could, because it works with your permissions.
  2. It builds on proven patterns. For typical club tasks it searches the curated example library (the source of the practical examples in chapters 11–13) and adapts the gold template to your field names and groups instead of starting from scratch.
  3. It writes and checks. It creates new scripts and modifies existing ones precisely. Before handing over, it checks the code for syntax errors and correct API usage and explains the flow to you. On open questions it asks instead of guessing — you watch its steps live in the panel and can pause it at any time.
  4. You stay in control. The assistant never executes scripts itself. Its output appears in the editor as a proposal version (see below) — nothing runs until you decide to.

One conversation per script: Every script keeps its own chat history — open it again later and continue seamlessly (“also add a date-range prompt”). With New conversation you start a free chat without a script attached; if the assistant creates a new script in it, the conversation is bound to that script automatically. Questions about the API or about a report are welcome too — it is teacher and programmer in one.

Scopes: where scripts live

Every script belongs to exactly one of the four levels from chapter 1 — and the assistant, too, always works within the level in which you opened the manager:

LevelOpen the managerManaged by
UserUser menuAutomationOnly you.
ProfileOn the organisation profile → AutomationManagers/admins of the profile.
GroupInside the group or its menu → AutomationGroup managers/admins; execution from member upwards (depending on release).
Event (folder)In the folder’s event administration → AutomationManagers/admins of the hosting profile.

The scope decides what a2d.context delivers, where the magic-wand menu offers the script, and how far vars.save reaches. Group scripts are the standard case; profile scripts suit jobs spanning several groups (e.g. creating whole club structures), and event scripts work at folder level — for instance to generate an event’s groups automatically.

Folder structure: order in the script tree

  • Folders & subfolders. The tree view on the left organises scripts in folders (one subfolder level). You create new items via the toolbar buttons Folder and Script — or via a folder’s context menu.
  • Rename, move, sort. Each entry’s context menu offers rename, delete, move to another folder, and up/down for the order — which also determines the order in the magic-wand menu.
  • References: one script, many places. Via Copy reference and Paste reference, an entry in another scope points live at the original — the code is read fresh from the source on every execution and is read-only in the editor (marked “Reference”). This lets you maintain a script centrally and use it in many groups; group templates also roll out their automations as references. Create copy turns the reference into an independent script when needed (the %GPIN% placeholder is replaced with the current template PIN in the process).

Triggers: how scripts are started

In the editor, Triggers opens the script’s trigger settings:

TriggerEffect
MenuThe script appears in the magic-wand menu of its level and is started manually from there.
AI assistantThe app’s support assistant may offer and run the script on request — your automation becomes a one-click answer.
Event triggerAutomatic start on status, data or attendance changes (configurable on group scripts) — details and parameters in chapter 10.

Release settings widen the circle of executors beyond the managers for the menu trigger: Also executable by responsibles (trainers start the script from the attendance list), Also executable by organisation participants (managers of registered clubs in federation groups) and Also executable in shared groups. For shared groups: if the share does not include the edit right, the script runs there read-only — writing a2d calls are rejected.

Versions: adopting changes safely

The editor keeps a version history for every open script. Every state — loaded from the server, edited by you, or proposed by the assistant — becomes its own version:

  1. Browse and compare. With more than one version, arrows appear in the toolbar (“2/3”) for stepping back and forth. The diff view highlights lines that changed compared to the previous version — you see at a glance what the assistant touched.
  2. Review the proposal. After an assistant run its output appears as a new version — not yet saved. Read the explanation in the chat, check the diff and run it via Execute — this is a real run with real data (if the script uses the member selection, the editor asks for it in a dialog beforehand).
  3. Adopt or discard. Save (Ctrl+S) makes the displayed version the valid state — Save all stores several changed scripts at once. To discard, simply step back to the desired version and save that one; closing with unsaved changes prompts you.

The history is a working aid, not an archive: It lives in memory while the automation manager is open — only the saved state persists. The execution history (last five runs per level) remains available independently via the history icon .

Putting it together: your complete toolbox

This closes the circle of the manual: you describe the task to the assistant, it builds on the gold examples and delivers a proposal version that you review in the diff, test and save. Via triggers you make the script available to the right people — from the magic-wand menu through the support assistant to the fully automatic event trigger. And with references and templates you roll the finished automation out to any number of groups, maintained centrally.

Read on: Administration, groups, data fields and fees are covered by the Administration Manual; the AI support assistant for your members is described in the User Manual.