AnyDB Scripting
This page documents the reusable scripting runtime used by AnyDB script features, including Automation Script Action and Record Customization Script (In Type designer).
Many Production Level Scripts are available for re-use
Supported Script Keywords (Globals)
| Keyword | Type | Description |
|---|---|---|
anydb | object | Data API for finding, creating, and updating records |
input | object | Host-provided input payload (shape depends on script feature/context) |
output | object | Report/output helper (markdown, text, json, table, csv) |
yieldNow | function | Cooperative yield helper for long loops (await yieldNow()) |
fetch | function | HTTP client for external API calls |
console | object | Logging object (console.log(...)) |
log | function | Shortcut for console.log(...) |
Available Script APIs
anydb.getRecordById(adoid)anydb.getRecordsByType(typeName)anydb.findRecords(queryOrParams)wherequeryOrParamscan be text or{ type, condition, querystring }anydb.findRecordsPage(queryOrParams)wherequeryOrParamscan be text or{ type, condition, querystring, cursor, limit }; returns paged results withnextCursoranydb.getChildren(parentid, options?)whereoptionscan include{ typename?, limit? }anydb.sendNotification({ to, message, from?, subject?, hyperlink?, sendEmail?, pushnotification? })sends in-app user notifications, and an email by default (supports comma-separated AnyDB user emails/user IDs).subjectsets the email subject;fromsets who the notification is from. Mobile push is opt-in withpushnotification: true.anydb.sendEmail({ to, subject, body, attachments? })sends email and raises in-app notification for resolvable AnyDB users into.attachmentstakes document handles fromanydb.generateDocumentand File record IDs.anydb.generateDocument({ adoid, formattedExportAdoid, asPdf? })renders a record through a Document Generation template (a PDF by default;asPdf: falsekeeps the template's.docx/.xlsxformat) and returns a document handle to attach withanydb.sendEmailor keep withanydb.attachDocumentanydb.attachDocument({ parent, document, name? })saves a document handle fromanydb.generateDocumentas a File record attached to theparentrecord, and returns the File record's IDanydb.createRecord({ name, parentid?, typeid?, typename?, cellValues? })anydb.updateRecord({ adoid, cellValues?, parentid? })anydb.createRecordShare({ adoid, visibility?, role?, withAttachments?, userIds?, emails?, groupIds?, name?, expiresAt? })anydb.updateShare({ adoid, shareId?, role?, withAttachments?, addUserIds?, addEmails?, addGroupIds?, removeUserIds?, removeEmails?, removeGroupIds?, name?, expiresAt?, visibility? })anydb.deleteShare({ shareId })anydb.createFormShare({ templateName, parentRecordId?, visibility?, userIds?, emails?, groupIds?, name?, expiresAt?, childForms?, submissionGrouping?, submissionNotifications? })anydb.updateFormShare({ shareId, name?, expiresAt?, visibility?, childForms?, submissionGrouping?, submissionNotifications?, addUserIds?, addEmails?, addGroupIds?, removeUserIds?, removeEmails?, removeGroupIds? })anydb.deleteFormShare({ shareId })anydb.getShare({ shareId, kind })anydb.addComment({ adoid, text, cell? })anydb.yield()(cooperative event-loop yield for long-running loops)output.summary(text)output.markdown(text)output.text(text)output.json(value)output.table(data)output.csv(data)output.set(key, value)(custom outputs for downstream actions)fetch(url, options)yieldNow()console.log(...)andlog(...)
API Example Index
- Data APIs: getRecordById, getRecordsByType, findRecords, findRecordsPage, getChildren, sendNotification, sendEmail, generateDocument, attachDocument, createRecord, updateRecord, createRecordShare, updateShare, deleteShare, createFormShare, updateFormShare, deleteFormShare, getShare, addComment, yield
- Output APIs: summary, markdown, text, json, table, csv, set
- Utilities: fetch, yieldNow, console.log / log
- Record helpers: getCell, getRefCellValue, setCell, setCellProps, setCellRefValue, lock, unlock, lockCell, unlockCell, hideCell, unhideCell
Quick Examples By API
anydb.getRecordById
Purpose: Load one record by its ADO ID.
// Signature: anydb.getRecordById(adoid)
const record = await anydb.getRecordById("67f1abc234def56789012345");
if (!record) {
output.summary("Record not found");
return;
}
log(`Loaded record ${record.id}: ${record.name}`);
anydb.getRecordsByType
Purpose: Fetch records for a type name when you need a broad type scan.
// Signature: anydb.getRecordsByType(typeName)
const invoices = await anydb.getRecordsByType("Invoice");
output.summary(`Invoices found: ${invoices.length}`);
anydb.findRecords
Purpose: Query records with filters or query text and return a result list.
// Signature: anydb.findRecords(queryOrParams)
const openIssues = await anydb.findRecords({
type: "Issue",
condition: 'STATE=="OPEN"',
limit: 100,
});
output.set("openIssueCount", openIssues.length);
anydb.findRecordsPage
Purpose: Query records in pages using cursor-based pagination for large datasets.
// Signature: anydb.findRecordsPage(queryOrParams)
const page1 = await anydb.findRecordsPage({
type: "Issue",
condition: 'STATE=="OPEN"',
limit: 50,
});
const page2 = page1.nextCursor
? await anydb.findRecordsPage({
type: "Issue",
condition: 'STATE=="OPEN"',
limit: 50,
cursor: page1.nextCursor,
})
: { records: [] };
output.summary(`Fetched ${page1.records.length + page2.records.length} rows`);
anydb.getChildren
Purpose: Get child records under a parent record, optionally constrained by type and limit.
// Signature: anydb.getChildren(parentid, options?)
const children = await anydb.getChildren(input.recordId, {
typename: "Invoice",
limit: 25,
});
log(`Child records: ${children.length}`);
anydb.sendNotification
Purpose: Send in-app notifications (and, by default, an email) to AnyDB users by email or user ID; mobile push is opt-in.
// Signature: anydb.sendNotification({ to, message, from?, subject?, hyperlink?, sendEmail?, pushnotification? })
const notify = await anydb.sendNotification({
to: "owner@company.com,67f1abc234def56789012345",
message: "Invoice requires review",
subject: "INV-1042 needs review",
hyperlink: "https://app.anydb.io/team/db/67f1abc234def56789012345",
pushnotification: true,
});
output.set("notify", notify);
anydb.sendEmail
Purpose: Send email to recipients and emit in-app notification for resolvable AnyDB users.
// Signature: anydb.sendEmail({ to, subject, body, attachments? })
const email = await anydb.sendEmail({
to: "finance@company.com,owner@company.com",
subject: "Daily invoice digest",
body: "The daily digest is ready.",
});
output.set("email", email);
To attach files, pass attachments: document handles returned by anydb.generateDocument in the same run, or IDs of File records. See Generate a Document and Email It.
anydb.generateDocument
Purpose: Generate a document from a record using a Document Generation template, the same way the Document Generation workflow action does, so it can be emailed or saved to a record. The output is a PDF unless you pass asPdf: false.
// Signature: anydb.generateDocument({ adoid, formattedExportAdoid, asPdf? })
const invoicePdf = await anydb.generateDocument({
adoid: input.recordId, // the record to generate from
formattedExportAdoid: "67f1abc234def56789012345", // the template's File record
});
// invoicePdf is a document handle, e.g. "generated-document://…"
// Keep the template's own format instead (a .docx template gives a .docx)
const invoiceDocx = await anydb.generateDocument({
adoid: input.recordId,
formattedExportAdoid: "67f1abc234def56789012345",
asPdf: false,
});
To keep the generated document, save it to a record with anydb.attachDocument. It becomes a File record attached to that record:
const fileId = await anydb.attachDocument({
parent: input.recordId, // the record to save it under
document: invoicePdf, // the handle from generateDocument
name: "Invoice 1042.pdf", // optional; include the extension
});
anydb.attachDocument
Purpose: Save a document from anydb.generateDocument to a record. The document becomes a File record attached to parent, listed with the record's other attachments.
// Signature: anydb.attachDocument({ parent, document, name? })
const invoicePdf = await anydb.generateDocument({
adoid: input.recordId,
formattedExportAdoid: "67f1abc234def56789012345",
});
const fileId = await anydb.attachDocument({
parent: input.recordId, // the record to attach it to
document: invoicePdf, // the handle from generateDocument
name: "Invoice 1042.pdf", // optional; include the extension
});
// fileId is the new File record's ID
See Generate a Document, Save It and Email It.
anydb.createRecord
Purpose: Create a new record and optionally initialize cell values.
// Signature: anydb.createRecord({ name, parentid?, typeid?, typename?, cellValues? })
const created = await anydb.createRecord({
name: "Invoice Follow-up",
typename: "Invoice",
cellValues: {
Status: "Draft",
Priority: "High",
},
});
log(`Created: ${created.id}`);
anydb.updateRecord
Purpose: Update field values or replace the parents of one existing record by ADO ID in the script's workspace.
// Signature: anydb.updateRecord({ adoid, cellValues?, parentid? })
const updated = await anydb.updateRecord({
adoid: "67f1abc234def56789012345",
cellValues: {
Status: "Ready",
},
});
log(`Updated: ${updated.id}`);
Use exact field names in cellValues. Omitted fields keep their current values.
The call returns the updated runtime record; its identifier is updated.id.
Always await the call.
Move a record under another record
Pass the destination record's ID as parentid. You can change the parent without
changing any fields:
const updated = await anydb.updateRecord({
adoid: input.recordId,
parentid: "67f1abc234def56789012346",
});
output.json(updated.meta.parent);
parentid accepts one ID or an array of IDs. It replaces the complete parent
list: the record is detached from any previous parent not included in that list.
There is no separate anydb.moveRecord scripting API.
To attach a record to another parent while preserving its existing parents, read the current list and include it in the update:
const record = await anydb.getRecordById(input.recordId);
if (!record) {
throw new Error("Record not found");
}
const additionalParentId = "67f1abc234def56789012346";
await anydb.updateRecord({
adoid: record.id,
parentid: [...new Set([...record.meta.parent, additionalParentId])],
});
Parent-update rules:
- Omit
parentidto leave the current parents unchanged. - An explicitly supplied
undefined,null, empty string, or empty array is rejected. An empty value does not mean "move to the workspace root". - To move to the root, supply the workspace root record's ID as the parent.
- You can supply
cellValuesandparentidin the same call. - The update runs as the workflow execution user through the record service; normal permissions and parent constraints still apply.
- Reading the parents and then updating them is not an atomic "add parent" operation. Coordinate concurrent changes so you do not replace a newer list with an older one.
Record titles and renaming
The scripting runtime does not currently expose a direct record-rename API.
anydb.updateRecord accepts adoid, cellValues, and parentid; name is not a
supported update parameter. The name accepted by anydb.createRecord applies
when creating a record, not when updating it.
record.name and record.meta.name are returned data. Assigning to them in
JavaScript does not save a new title. Likewise, a field named Name is a cell,
not the record's title metadata.
If the record has a title formula that uses an editable field, update that field
to change the input to the title formula. For example, for a type whose title
formula is CONCAT({{Name}}):
const updated = await anydb.updateRecord({
adoid: input.recordId,
cellValues: {
Name: "October invoice",
},
});
log(`Record title: ${updated.name}`);
This example requires that title formula and an editable Name field. Updating
a Name cell on a type without that title formula does not by itself rename the
record. For a literal record title, use the application's record-title editing
controls; scripts cannot directly set it through the APIs on this page.
Records containing files
A record's title and an uploaded file's filename are separate information. A record may contain a file cell, or have File records attached beneath it. Changing a title field updates that field and any title formula depending on it; it is not a file-rename operation and does not rename attached child records.
For example, an invoice titled September invoice might contain a file uploaded
as invoice-123.pdf. Changing the invoice's title to October invoice does not
request a change to the uploaded file's filename or replace its contents. The
same distinction applies to the title of a File record itself.
The scripting runtime has no supported API for renaming an uploaded file. Do not
try to rename one by changing record.name, by adding name to updateRecord,
or by replacing a file cell with a filename string. File cells contain an upload
reference and file metadata, not just a text name. A different download filename
requires a file-specific capability; a record-title change is not sufficient.
anydb.createRecordShare
Purpose: Create a share for an existing record. To share a form, see createFormShare.
// Signature: anydb.createRecordShare({ adoid, visibility?, role?, withAttachments?, userIds?, emails?, groupIds?, name?, expiresAt? })
const privateShare = await anydb.createRecordShare({
adoid: "67f1abc234def56789012345",
visibility: "private",
role: "editor",
withAttachments: true,
userIds: ["67f1abc234def56789012346"],
emails: ["reviewer@company.com"],
groupIds: ["67f1abc234def56789012347"],
name: "Invoice review",
expiresAt: "2030-12-31",
});
output.set("shareId", privateShare.shareId);
Set visibility to "public" to create a public link:
const publicShare = await anydb.createRecordShare({
adoid: input.recordId,
visibility: "public",
role: "viewer",
expiresAt: "2030-12-31T23:59:59Z",
});
output.set("publicShareUrl", publicShare.shareUrl);
Creation notes:
visibilitydefaults to"private";roledefaults to"viewer".- Private shares can target existing users, email addresses, and groups. AnyDB handles invitation or pending-account creation for email recipients.
withAttachmentsdefaults tofalse.expiresAtaccepts a valid date string and is optional.- Every share returns
url, the link to give people: a public share's/s/link, or, for a private share, the page where the people it is shared with find it under "Shared with me". Public shares also returnshareUrl. - Return shape:
{ shareId, visibility, name, url, shareUrl?, userIds, groupIds, expiresAt? }.
anydb.updateShare
Purpose: Change an existing record share: rename it, change or remove its expiry, turn its public link on or off, and add or remove users, email recipients and groups.
// Signature: anydb.updateShare({ adoid, shareId?, role?, withAttachments?, addUserIds?, addEmails?, addGroupIds?, removeUserIds?, removeEmails?, removeGroupIds?, name?, expiresAt?, visibility? })
const updatedShare = await anydb.updateShare({
adoid: "67f1abc234def56789012345",
role: "editor",
withAttachments: true,
addEmails: ["new-reviewer@company.com"],
addGroupIds: ["67f1abc234def56789012349"],
removeUserIds: ["67f1abc234def56789012346"],
removeEmails: ["former-reviewer@company.com"],
name: "Invoice review (round 2)",
expiresAt: "2030-12-31",
});
if (updatedShare.deleted) {
output.summary("Share deleted after its final recipient was removed");
} else {
output.set("sharedUserIds", updatedShare.userIds);
}
Update notes:
adoidis required. The share is found from it; passshareIdonly when you have a real one, and it must match that record's share.roleandwithAttachmentsapply to the people added in this call.namerenames the share.expiresAtis a date (YYYY-MM-DD, today or later) that sets the expiry, ornullto remove it; leave it out to keep the current expiry. Adding access also keeps the current expiry.visibility: "public"turns the public link on;"private"turns it off and keeps the named people.- Email additions use AnyDB's normal invitation handling, and only people newly added are emailed. Email removals resolve the existing AnyDB user by email.
- Only the person who created a share, which for a workflow is its execution user, can change its name, expiry or public link.
- Removing the final target from a private share can delete the share and return
{ shareId, deleted: true }; otherwise the return shape matchescreateRecordShare.
anydb.deleteShare
Purpose: Delete an existing record share and revoke access granted by it. To delete a form share, use deleteFormShare.
// Signature: anydb.deleteShare({ shareId })
const result = await anydb.deleteShare({
shareId: "67f1abc234def56789012348",
});
output.set("shareDeleted", result.deleted);
The return shape is { deleted: true }. Share operations run as the workflow execution user and are subject to normal AnyDB authorization rules: see Sharing access rules.
anydb.createFormShare
Purpose: Share a form, so that people can submit records of a type, optionally together with child records, and get the link to give them.
// Signature: anydb.createFormShare({ templateName, parentRecordId?, visibility?, userIds?, emails?, groupIds?, name?, expiresAt?, childForms?, submissionGrouping?, submissionNotifications? })
const form = await anydb.createFormShare({
templateName: "Safety Report",
name: "Safety report intake",
emails: ["contractor@company.com"],
groupIds: ["67f1abc234def56789012347"],
expiresAt: "2030-12-31",
childForms: [{ templateName: "Photo", min: 1, max: 4 }],
submissionGrouping: "MONTH",
submissionNotifications: true,
});
output.set("formShareId", form.shareId);
output.set("formLink", form.url);
Creation notes:
templateNameis the stable name of the type people fill in.- Submissions are created under
parentRecordId. If you leave it out, AnyDB creates a folder for them at the workspace root. visibilitydefaults to"private", which needs at least one user, email or group."public"gives an open link and must not name anyone.childFormslists child types the submitter fills in together with the form:templateName, and optionallymin(default0) andmax(default1).submissionGroupingis"NONE"(the default),"DAY","WEEK","MONTH"or"YEAR", and files submissions into a new folder for each period.submissionNotificationsturns the email to the sharer on each submission on or off; it defaults totrue.expiresAtis a date (YYYY-MM-DD, today or later).- Creating a form share again for the same parent updates that share and adds the people named. Use updateFormShare to change or remove people.
- Each plan allows a number of form shares. At the limit the call fails with a message that names the limit.
- Return shape:
{ shareId, kind: "form", visibility, name, url, expiresAt, parentRecordId, templateName, userIds, groupIds, recipients, childForms, submissionGrouping, submissionNotifications }.urlis the form's link.
anydb.updateFormShare
Purpose: Change an existing form share: its settings, and who it is shared with.
// Signature: anydb.updateFormShare({ shareId, name?, expiresAt?, visibility?, childForms?, submissionGrouping?, submissionNotifications?, addUserIds?, addEmails?, addGroupIds?, removeUserIds?, removeEmails?, removeGroupIds? })
const updated = await anydb.updateFormShare({
shareId: form.shareId,
submissionGrouping: "WEEK",
expiresAt: null,
childForms: [],
addEmails: ["second-contractor@company.com"],
removeEmails: ["contractor@company.com"],
});
output.set("formRecipients", updated.recipients.users.map((u) => u.email));
Update notes:
- Only what you name changes; everything else is kept.
expiresAt: nullremoves the expiry, andchildFormsreplaces the whole child-form list ([]removes them). visibility: "public"turns the public link on;"private"turns it off and keeps the named people.- Only people newly added are emailed. Email removals use the address the person was shared with; an address that is not on the share is an error.
- Adding a person to an existing form share works even when the team is at its form-share limit.
- A form share stays in place when its last person is removed.
- The share must be in the workflow's own workspace. Returns the same shape as
createFormShare.
anydb.deleteFormShare
Purpose: Delete a form share, which closes its link and removes the access it granted.
// Signature: anydb.deleteFormShare({ shareId })
const result = await anydb.deleteFormShare({ shareId: form.shareId });
output.set("formShareDeleted", result.deleted);
The return shape is { deleted: true }. In a simulated run the share is checked but not deleted, so a wrong ID or a missing permission shows up before the real run.
anydb.getShare
Purpose: Read one share, either kind, including who it is shared with, before you add or remove people.
// Signature: anydb.getShare({ shareId, kind }) // kind: "record" or "form"
const share = await anydb.getShare({ shareId: form.shareId, kind: "form" });
const emails = share.recipients.users.map((u) => u.email);
output.set("sharedWith", emails);
Return shape: { shareId, kind, visibility, name, url, expiresAt, userIds, groupIds, recipients: { users: [{ userId, email?, name?, role? }], groups: [{ groupId, name? }] } }, plus childForms, submissionGrouping and submissionNotifications for a form share. If the workflow's execution user may read a share but not manage it, recipients is empty and recipientsHidden is true. Reading works in a simulated run too, because it changes nothing.
Sharing access rules
Every share function runs as the workflow's execution user and applies the same access rules as the Share dialog. Nothing in a script can share, change or read a share that the user could not in the dialog:
- Creating, changing or deleting a share needs permission to share the record (for a form, its parent record).
- Reading a share needs permission to read what it shares. Who else it is shared with is shown only to the person who created it and to people who may share the record.
- A script can only reach shares in its own workspace.
- A failure carries the reason ("not authorized", "not found", a plan limit), so a script can tell them apart.
anydb.addComment
Purpose: Add a comment to a record, or to one of its cells, the way a person does in the comments panel.
// Signature: anydb.addComment({ adoid, text, cell? })
const record = await anydb.getRecordById(input.recordId);
// On the record itself
await anydb.addComment({
adoid: record.id,
text: "Approved automatically: total is under the limit.",
});
// On one cell's thread (a position such as "A8", or the cell's key)
const posted = await anydb.addComment({
adoid: record.id,
cell: "Discussion",
text: "Please confirm the delivery date with [Sam Lee](user://67f1abc234def56789012346).",
});
output.set("commentId", posted.commentId);
Comment notes:
adoidandtextare required.textcannot be empty.- Omit
cellto comment on the record itself. A record's own thread and each cell's thread are separate, so read the thread you mean withgetCommentsif you need to see what is already there. - It adds one comment and never touches the ones already in the thread. To replace a whole thread, use
setComments, which does not append. - The author is the workflow's execution user, and the comment gets its own ID and date. A script cannot post as someone else.
- The execution user needs permission to update the record, the same rule as commenting in the UI. A refused call fails with the reason and adds nothing.
- Like a comment made in the UI, it emails the record's creator and assignees (not the commenter) and anyone mentioned with
[Name](user://<userid>). It also counts as an update to the record. - Return shape:
{ commentId, scope: "record" | "cell", cell? }. In a simulated run the record, cell and permission are checked but nothing is posted, and it returns{ commentId: null, scope, simulated: true }.
anydb.yield
Purpose: Cooperatively yield the event loop during heavy loops.
// Signature: anydb.yield()
const records = await anydb.getRecordsByType("Invoice");
for (const record of records) {
await anydb.yield();
// heavy per-record logic
}
output.summary(`Processed ${records.length}`);
output.summary
Purpose: Set a high-level completion summary for script output.
// Signature: output.summary(text)
output.summary("Script completed successfully");
output.markdown
Purpose: Emit markdown-formatted output.
// Signature: output.markdown(text)
output.markdown("## Invoice Summary\n- Open: 12\n- Closed: 34");
output.text
Purpose: Emit plain text output.
// Signature: output.text(text)
output.text("Invoice reconciliation completed");
output.json
Purpose: Emit structured JSON output.
// Signature: output.json(value)
output.json({
runAt: Date.now(),
success: true,
updatedCount: 42,
});
output.table
Purpose: Emit tabular output from row data.
// Signature: output.table(data)
output.table([
{ name: "Invoice A", amount: 1000, status: "Open" },
{ name: "Invoice B", amount: 500, status: "Closed" },
]);
output.csv
Purpose: Emit CSV-formatted output from row data.
// Signature: output.csv(data)
output.csv([
{ id: "INV-1", amount: 1000 },
{ id: "INV-2", amount: 500 },
]);
output.set
Purpose: Store custom named outputs for downstream actions.
// Signature: output.set(key, value)
output.set("updatedCount", 42);
output.set("status", "success");
fetch
Purpose: Call external HTTP APIs and use the response inside your script.
// Signature: fetch(url, options)
const response = await fetch("https://api.example.com/rates?base=USD");
if (!response.ok) {
throw new Error(`Rate API failed with ${response.status}`);
}
const payload = await response.json();
output.set("rates", payload);
yieldNow
Purpose: Yield control manually in custom loops (utility alias for cooperative yielding).
// Signature: yieldNow()
for (let i = 0; i < 1000; i++) {
if (i % 100 === 0) {
await yieldNow();
}
}
output.summary("Loop completed");
console.log / log
Purpose: Write diagnostic lines to script logs.
// Signatures: console.log(...args), log(...args)
console.log("Debug", { step: "start" });
log("A shorter log call");
Record-Level Helpers
Record objects returned by anydb.getRecordById(...), anydb.getRecordsByType(...), anydb.findRecords(...), and anydb.getChildren(...) expose:
record.getCell(refOrKey)record.getRefCellValue(refPath)whererefPathcan be a string path ("A1->B1->D1") or string array (["A1", "B1", "D1"])record.setCell(refOrKey, value)record.setCellProps(refOrKey, props)record.setCellRefValue(refOrKey, adoid)(loads target record and writes normalized REF payload + REF expression)record.lock(options?)/record.unlock(options?)whereoptionscan include{ recursive: true }to also update all descendants recursivelyrecord.lockCell(refOrKey)/record.unlockCell(refOrKey)record.hideCell(refOrKey)/record.unhideCell(refOrKey)
record.getCell
Purpose: Read one cell by key or cell position from an already loaded record.
// Signature: record.getCell(refOrKey)
const host = await anydb.getRecordById(input.recordId);
const cell = host ? host.getCell("Status") : null;
log(`Status cell value: ${cell ? cell.value : "N/A"}`);
if (cell) {
log(`Status cell pos: ${cell.pos}`);
log(`Status cell locked: ${Boolean(cell.props?.CELL_LOCKED?.value)}`);
}
Returned cell objects expose pos, key, format, value, and props.
record.getRefCellValue
Purpose: Get a referenced record's cell value, following a REF chain when each hop is itself a REF (for example: host.getRefCellValue("Source Item Ref->Location Ref->Name")).
// Signature: record.getRefCellValue(refPath)
const host = await anydb.getRecordById(input.recordId);
const refTargetValue = host
? await host.getRefCellValue("Source Item Ref->Location Ref->Name")
: null;
output.set("resolvedLocationName", refTargetValue);
record.setCell
Purpose: Set one cell value on the current record by key or position.
// Signature: record.setCell(refOrKey, value)
const host = await anydb.getRecordById(input.recordId);
if (host) {
await host.setCell("Validation State", "OK");
}
record.setCellProps
Purpose: Update props on an existing cell by key or position. This is the low-level helper behind cell locking and visibility.
// Signature: record.setCellProps(refOrKey, props)
const host = await anydb.getRecordById(input.recordId);
if (host) {
await host.setCellProps("Approval Status", {
CELL_LOCKED: {
type: "boolean",
value: true,
},
});
}
Use this when you want to set props explicitly. Prefer record.lockCell(...), record.unlockCell(...), record.hideCell(...), and record.unhideCell(...) for those common operations.
record.setCellRefValue
Purpose: Set a REF cell by target ADO ID and let runtime build the normalized REF value payload.
// Signature: record.setCellRefValue(refOrKey, adoid)
const host = await anydb.getRecordById(input.recordId);
if (host) {
await host.setCellRefValue("Source Location Ref", "6a0b639aa6afd82c3adfcbb0");
}
record.lock
Purpose: Lock the current record. Pass { recursive: true } to also lock child records recursively.
// Signature: record.lock(options?)
const host = await anydb.getRecordById(input.recordId);
if (host && !host.meta.locked) {
await host.lock({ recursive: true });
output.summary(`Locked record ${host.name}`);
}
Use record.lock() when you only want to lock the current record. Use record.lock({ recursive: true }) when the entire descendant tree should be locked together.
record.unlock
Purpose: Unlock the current record. Pass { recursive: true } to also unlock child records recursively.
// Signature: record.unlock(options?)
const host = await anydb.getRecordById(input.recordId);
if (host && host.meta.locked) {
await host.unlock({ recursive: true });
output.summary(`Unlocked record ${host.name}`);
}
Use record.unlock() when you only want to unlock the current record. Use record.unlock({ recursive: true }) when the entire descendant tree should be unlocked together.
record.lockCell
Purpose: Lock one cell by key or position so subsequent value updates are rejected. This is equivalent to setting CELL_LOCKED to true.
// Signature: record.lockCell(refOrKey)
const host = await anydb.getRecordById(input.recordId);
if (host) {
await host.lockCell("Approval Status");
}
record.unlockCell
Purpose: Unlock one cell by key or position so it can be updated again. This is equivalent to setting CELL_LOCKED to false.
// Signature: record.unlockCell(refOrKey)
const host = await anydb.getRecordById(input.recordId);
if (host) {
await host.unlockCell("B4");
}
record.hideCell
Purpose: Hide one cell by key or position without deleting its stored value. This sets its CELL_HIDDEN prop to true.
// Signature: record.hideCell(refOrKey)
const host = await anydb.getRecordById(input.recordId);
if (host) {
await host.hideCell("Internal Notes");
}
record.unhideCell
Purpose: Unhide one cell by key or position. This sets its CELL_HIDDEN prop to false.
// Signature: record.unhideCell(refOrKey)
const host = await anydb.getRecordById(input.recordId);
if (host) {
await host.unhideCell("B4");
}
Cell state helper notes:
- All four helpers accept a cell key (for example
"Approval Status") or position (for example"B4"). - Always
awaitthese methods. Each returns the refreshed record after the property update. lockCellandunlockCellchange editability throughCELL_LOCKED; they do not change visibility.hideCellandunhideCellchange visibility throughCELL_HIDDEN; hiding does not delete the cell or clear its stored value.- Calls run as the workflow execution user and require normal update permission on the record.
- Use
setCellProps(...)only when you need lower-level property control.
Cell State Example: Protect And Hide Approval Fields
Purpose: Lock an approved value and hide an internal note, then reverse both changes when the record is reopened.
const host = await anydb.getRecordById(input.recordId);
if (!host) {
output.summary("Record not found");
return;
}
if (input.reopened === true) {
await host.unlockCell("Approval Status");
await host.unhideCell("Internal Approval Notes");
output.summary("Approval fields reopened");
return;
}
await host.lockCell("Approval Status");
await host.hideCell("Internal Approval Notes");
output.summary("Approval fields protected");
Locking Example: Lock A Record Tree And Specific Cells
Purpose: Show a full pattern for locking a record tree while selectively locking and unlocking cells on the host record.
const host = await anydb.getRecordById(input.recordId);
if (!host) {
output.summary("Record not found");
return;
}
await host.lock({ recursive: true });
await host.lockCell("Approval Status");
await host.lockCell("B4");
await host.unlockCell("Comments");
output.summary(
`Record tree for ${host.name} locked with targeted cell exceptions updated`,
);
Locking Example: Unlock A Record During Reopen Flow
Purpose: Reopen a record tree and make a previously protected cell editable again.
const host = await anydb.getRecordById(input.recordId);
if (!host) {
output.summary("Record not found");
return;
}
await host.unlock({ recursive: true });
await host.unlockCell("Approval Status");
output.summary(`Record tree for ${host.name} unlocked for editing`);
Input Shape By Context
Workflow Script Action
inputis action-driven payload (for example custom fields and upstream mapped values)input.recordIdmay or may not be present depending on trigger/action wiring- Use
output.summary(...),output.set(...), table/json/csv helpers for downstream actions and reports
Record customization Script
input.recordIdis the host record being recomputedinput.changedCellsis available to identify what changed on this update- Record customization logic runs on record updates, so always guard early and return quickly when not relevant
Record Data Access Model (Restricted)
Scripts do not get raw ADO objects. Each record returned by anydb.* APIs exposes only:
record.idrecord.namerecord.urlrecord.cellValues(field key/position values)record.metawith only these fields:parentassigneesnametemplateIDtemplateNametemplateVersionlockedcreatedcreatedbyupdatedupdatedby
No other metadata fields are exposed to script runtime.
How to Access Record Details
const recs = await anydb.findRecords({
type: "AnyDB Issue",
});
for (const record of recs) {
await anydb.yield();
// Basic record fields
const recordId = record.id;
const recordName = record.name;
const recordUrl = record.url;
// Cell values (by key or position)
const title = record.cellValues["TITLE"];
const ticketState = record.cellValues["TICKET STATE"];
const posA1Value = record.cellValues["A1"];
// Date fields need to be unix time stamp in seconds
const newDate = Date.now() / 1000;
// Allowed meta fields
const parents = record.meta.parent; // array of parent IDs
const assignees = record.meta.assignees; // { users: [...], groups: [...] }
const templateId = record.meta.templateID;
const templateName = record.meta.templateName;
const templateVersion = record.meta.templateVersion;
const isLocked = record.meta.locked;
const createdAt = record.meta.created;
const createdBy = record.meta.createdby;
const updatedAt = record.meta.updated;
const updatedBy = record.meta.updatedby;
log(
`Record ${recordId} | name=${recordName} | type=${templateName} (${templateId}) v${templateVersion} | locked=${isLocked} | state=${ticketState} | parentCount=${parents.length}`,
);
}
Script Structures
Structure: Workflow Script Action
// 1) Read workflow/action input
const typeName = String(input.typeName || "Invoice");
// 2) Query records
const records = await anydb.findRecords({
type: typeName,
condition: 'Status=="Open"',
});
// 3) Process and update as needed
let updated = 0;
for (const record of records) {
await anydb.yield();
await anydb.updateRecord({
adoid: record.id,
cellValues: {
Status: "Processed",
},
});
updated += 1;
}
// 4) Emit action outputs for downstream steps
output.summary(`Processed ${updated} records`);
output.set("updatedCount", updated);
Structure: Record customization Script
// 1) Monitor only specific triggering cells
const changedCells = Array.isArray(input.changedCells)
? input.changedCells
: [];
const watchedField = "Source item Ref";
const watchedTriggered = changedCells.some(
(ref) => String(ref || "").toLowerCase() === watchedField.toLowerCase(),
);
if (!watchedTriggered) {
return; // silent no-op on unrelated updates
}
// 2) Validate host record context
const recordId = String(input.recordId || "").trim();
if (!recordId) {
output.cellValue("BAD");
return;
}
// 3) Read host record and evaluate logic
const hostRecord = await anydb.getRecordById(recordId);
if (!hostRecord) {
output.cellValue("BAD ID");
return;
}
// Example: read and write on same host record
const sourceRef = hostRecord.getCell("Source item Ref");
if (!sourceRef || !sourceRef.value) {
output.cellValue("MISSING SOURCE");
return;
}
// Optional helper for REF writes
// await hostRecord.setCellRefValue("Source Location Ref", "6a0b639aa6afd82c3adfcbb0");
// Regular updates on host record
await hostRecord.setCell("Validation State", "OK");
// 4) Set script-cell value (or omit for silent behavior if desired)
output.cellValue("OK");
Script Authoring Policies
These policies are recommended when handcrafting scripts.
Workflow Script Action Policy
- Use only documented runtime APIs. Do not call unsupported helpers such as
anydb.fetchRecordsByTemplateName(...),anydb.query(...), oroutput.html(...). - All
anydb.*data APIs are async. Alwaysawaitreturned values before using them. - For criteria-based queries, prefer
anydb.findRecords(...)oranydb.findRecordsPage(...). - For full type scans without filters,
anydb.getRecordsByType(...)is acceptable. - For
findRecordsandfindRecordsPage, use exactly one type selector:typeORtypeNameORtemplateName. - Do not use
templateIdinside script runtime query params. - Use
conditionwith comparison operators (==,!=,>=,<=, etc.). Avoid single=in conditions. - In long loops, use
await anydb.yield()to keep execution responsive. - Prefer compact outputs for downstream actions:
output.summary(...)andoutput.set(...).
Record customization Script Policy
- Treat Record customization script as update-reactive logic that may run frequently.
- Use
input.changedCellsas trigger context and return early when monitored cells are not part of the update. - Resolve host context via
input.recordIdandawait anydb.getRecordById(input.recordId). - For host-record updates, prefer
record.setCell(...)andrecord.setCellRefValue(...). - Do not use
output.set(...),output.summary(...),output.markdown(...),output.text(...),output.json(...),output.table(...) - Keep Record customization scripts targeted and lightweight; avoid broad full-database scans unless absolutely necessary.
record.setCell(...)supports exactly two arguments:(refOrKey, value).
Create + Update Example
const invoice = await anydb.createRecord({
name: "Invoice Follow-up",
typename: "Invoice",
cellValues: {
Status: "Draft",
Currency: "USD",
},
});
await anydb.updateRecord({
adoid: invoice.id,
cellValues: {
Status: "Ready",
},
});
log(`Created and updated record: ${invoice.id}`);
Find + Update Loop Example
const invoices = await anydb.findRecords({
type: "Invoice",
condition: 'Status=="Open"',
});
let updated = 0;
for (const invoice of invoices) {
await anydb.yield();
const amount = Number(invoice.cellValues["Amount"] || 0);
const taxRate = Number(invoice.cellValues["Tax Rate"] || 0);
const total = amount + amount * taxRate;
await anydb.updateRecord({
adoid: invoice.id,
cellValues: {
Total: total,
Status: "Recalculated",
},
});
updated += 1;
}
log(`Updated ${updated} invoices`);
API Fetch + Update Example
const apiResponse = await fetch("https://currency.rate.com?base=USD");
if (!apiResponse.ok) {
throw new Error(`Exchange API failed: ${apiResponse.status}`);
}
const data = await apiResponse.json();
const conversionRate = Number(data?.rates?.EUR || 0);
if (!conversionRate) {
throw new Error("EUR conversion rate is missing from API response");
}
const invoices = await anydb.findRecords({
type: "Invoice",
condition: 'Currency=="USD" AND Status=="Open"',
});
for (const invoice of invoices) {
await anydb.yield();
const amountUSD = Number(invoice.cellValues["Amount"] || 0);
const amountEUR = Number((amountUSD * conversionRate).toFixed(2));
await anydb.updateRecord({
adoid: invoice.id,
cellValues: {
"Amount EUR": amountEUR,
"FX Rate EUR": conversionRate,
},
});
}
log(`Updated ${invoices.length} invoices with EUR conversion`);
CSV Example
const recs = await anydb.findRecords({
type: "Invoice",
condition: 'Status=="Overdue"',
});
const rows = recs.map((record) => ({
id: record.id,
name: record.name,
amount: record.cellValues["Amount"],
dueDate: record.cellValues["Due Date"],
}));
output.csv(rows);
log(`Generated CSV rows: ${rows.length}`);
Custom Outputs + Downstream Mapping Example
const overdue = await anydb.findRecords({
type: "Invoice",
condition: 'Status=="Overdue"',
});
output.set("overdueCount", overdue.length);
output.set(
"overdueHtml",
`<p>Overdue invoices: <strong>${overdue.length}</strong></p>`,
);
Get Children Example
const parentId = input.recordId;
const children = await anydb.getChildren(parentId, {
typename: "Invoice",
limit: 25,
});
for (const child of children) {
await anydb.yield();
await anydb.updateRecord({
adoid: child.id,
cellValues: {
Status: "Reviewed",
},
});
}
log(`Processed ${children.length} child records`);
Send Notification Example
const result = await anydb.sendNotification({
to: "owner@company.com,67f1abc234def56789012345",
message: "Invoice requires attention",
hyperlink: "https://app.anydb.io/workspace/invoices/67f1abc234def56789012345",
});
output.set("notificationResult", result);
log(
`Notifications requested=${result.requestedRecipients}, delivered=${result.notifiedUsers}`,
);
Notification notes:
toaccepts comma-separated AnyDB user emails and/or user IDs.- Only resolvable AnyDB users are notified.
hyperlinkis optional and defaults to empty when omitted.sendEmailis optional and defaults totrue: each notified user also gets an email. SetsendEmail: falsefor in-app only.subjectis optional and sets the email's subject line. When omitted, the subject is the start of the message (about 80 characters, cut on a word boundary). Give a subject when the message starts with something generic. A record name works well, and it keeps one record's notifications together in the recipient's email thread. The subject is always a single line.- The email body names the sender ("Anis Abdul sent you a message") above the message.
fromis optional and sets who the notification is from: a user ID or the email of a member of the workflow's team. When omitted, the sender is the workflow's owner, not the person who triggered it. In a record-update workflow, passrecord.meta.updatedbyso the notification names whoever made the change.pushnotificationis optional and defaults tofalsein scripting runtime; setpushnotification: trueto explicitly send mobile push.
Record-update example: notify the record's owner, from whoever changed it, with the record's name as the subject:
const record = await anydb.getRecordById(input.recordId);
await anydb.sendNotification({
to: record.meta.createdby,
from: record.meta.updatedby,
subject: record.name,
message: `${record.name} was updated.`,
hyperlink: record.url,
});
Send Email Example
const result = await anydb.sendEmail({
to: "owner@company.com,finance@company.com",
subject: "Invoice summary ready",
body: "Automation completed the invoice reconciliation run.",
});
output.set("emailResult", result);
log(
`Email recipients=${result.emailedRecipients}, notifiedUsers=${result.notifiedUsers}`,
);
Send email notes:
toaccepts comma-separated email recipients.- Email is sent using
email.general.message. - In-app notification is raised for recipients that resolve to AnyDB users by email.
- Return shape:
{ emailedRecipients, notifiedUsers }.
Generate a Document and Email It
Generate an invoice PDF from the record that triggered the workflow and email it to the customer:
const invoice = await anydb.getRecordById(input.recordId);
const invoicePdf = await anydb.generateDocument({
adoid: invoice.id,
formattedExportAdoid: "67f1abc234def56789012345", // Invoice template's File record
});
const result = await anydb.sendEmail({
to: invoice.getCell("Customer Email").value,
subject: `Invoice ${invoice.name}`,
body: "Please find your invoice attached.",
attachments: [invoicePdf],
});
output.set("emailResult", result);
Generate document notes:
adoidis the record to generate from.formattedExportAdoidis the Document Generation template: the File record holding the.docxor.xlsxuploaded under Document Generation in the workspace settings.- The output is a PDF by default, matching the Document Generation workflow action. Pass
asPdf: falseto get the document in the template's own format (.docxor.xlsx); it attaches tosendEmailthe same way. - The workflow's execution user must be able to read both the record and the template.
generateDocumentreturns a document handle (generated-document://…), not a file path. The handle is only valid in the script run that created it; use it withsendEmailorattachDocumentin the same run. The document is not saved to any record unless you pass it toattachDocument.- If the template is missing or cannot be rendered,
generateDocumentthrows with the reason. - In a simulate (dry run) nothing is generated and no email is sent. The handle it returns still passes
sendEmail's attachment checks, so the preview runs through.
Email attachment notes:
attachmentsis an array (a comma-separated string also works) of document handles fromgenerateDocumentin this run and/or IDs of File records the execution user can read.- Up to 10 attachments per email, 5MB each.
- An email with attachments is sent during the script run (
queued: false), so a delivery error reaches the script.
Generate a Document, Save It and Email It
Generate an invoice PDF, keep it on the invoice record, and email the saved copy:
const invoice = await anydb.getRecordById(input.recordId);
const invoicePdf = await anydb.generateDocument({
adoid: invoice.id,
formattedExportAdoid: "67f1abc234def56789012345", // Invoice template's File record
});
const fileId = await anydb.attachDocument({
parent: invoice.id,
document: invoicePdf,
name: `Invoice ${invoice.name}.pdf`,
});
await anydb.sendEmail({
to: invoice.getCell("Customer Email").value,
subject: `Invoice ${invoice.name}`,
body: "Please find your invoice attached.",
attachments: [fileId], // the saved File record; the handle works too
});
output.set("invoiceFile", fileId);
Attach document notes:
parentis the record the document is attached to. It can be any record, not only the one the document was generated from.documentmust be a handlegenerateDocumentreturned in the same run. A file path or an existing File record ID is refused.namesets the File record's name and file name. Include the extension. It defaults to the generated file's name.- The File record is created as the workflow's execution user. If the parent doesn't exist, or that user can't add records under it,
attachDocumentthrows. - The returned ID is a File record, so you can pass it to
sendEmailinattachments, or store it in a cell. - In a simulate (dry run) nothing is saved.
attachDocumentreturns asimulated-…ID, whichsendEmailaccepts in the same simulate.
Runtime Safety & Abuse Prevention
- Script execution runs with timeout protection. Timeout limits are enforced by the host feature (for example, Script Action).
- Obvious tight infinite loops are blocked before execution (for example
while(true),while(1), andfor(;;)). - Loop statements (
for,while,do...while,for...of,for...in) must include at least oneawaitin the loop body. - For long-running loops, explicitly yield using
await anydb.yield()(orawait yieldNow()) to prevent event-loop starvation. - Security validation blocks restricted patterns such as
import,export,require(...),process, global/module internals, dynamic code execution (eval,Function), and constructor-escape patterns. - Only the supported runtime surface is available to scripts (
anydb,input,output,yieldNow,fetch,console,log;baseis retained as a compatibility alias). Filesystem and Node system-module access are not allowed. - Email attachments can never be used to send files from the server:
sendEmailnever accepts a file path, only document handles fromgenerateDocumentin the same run and File record IDs.- A handle can't be forged or reused in another run.
- A File record is read as the workflow's execution user, so record permissions apply.
- Every resolved attachment must be a regular file (not a symlink) inside the server's temporary scratch folder and at most 5MB.
- Anything else is refused before any email is sent, including in a simulate.