Skip to main content

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).

Example Scripts

Many Production Level Scripts are available for re-use

Automation Script Library

Supported Script Keywords (Globals)​

KeywordTypeDescription
anydbobjectData API for finding, creating, and updating records
inputobjectHost-provided input payload (shape depends on script feature/context)
outputobjectReport/output helper (markdown, text, json, table, csv)
yieldNowfunctionCooperative yield helper for long loops (await yieldNow())
fetchfunctionHTTP client for external API calls
consoleobjectLogging object (console.log(...))
logfunctionShortcut for console.log(...)

Available Script APIs​

  • anydb.getRecordById(adoid)
  • anydb.getRecordsByType(typeName)
  • anydb.findRecords(queryOrParams) where queryOrParams can be text or { type, condition, querystring }
  • anydb.findRecordsPage(queryOrParams) where queryOrParams can be text or { type, condition, querystring, cursor, limit }; returns paged results with nextCursor
  • anydb.getChildren(parentid, options?) where options can 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). subject sets the email subject; from sets who the notification is from. Mobile push is opt-in with pushnotification: true.
  • anydb.sendEmail({ to, subject, body, attachments? }) sends email and raises in-app notification for resolvable AnyDB users in to. attachments takes document handles from anydb.generateDocument and File record IDs.
  • anydb.generateDocument({ adoid, formattedExportAdoid, asPdf? }) renders a record through a Document Generation template (a PDF by default; asPdf: false keeps the template's .docx/.xlsx format) and returns a document handle to attach with anydb.sendEmail or keep with anydb.attachDocument
  • anydb.attachDocument({ parent, document, name? }) saves a document handle from anydb.generateDocument as a File record attached to the parent record, and returns the File record's ID
  • anydb.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(...) and log(...)

API Example Index​

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 parentid to 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 cellValues and parentid in 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:

  • visibility defaults to "private"; role defaults to "viewer".
  • Private shares can target existing users, email addresses, and groups. AnyDB handles invitation or pending-account creation for email recipients.
  • withAttachments defaults to false.
  • expiresAt accepts 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 return shareUrl.
  • 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:

  • adoid is required. The share is found from it; pass shareId only when you have a real one, and it must match that record's share.
  • role and withAttachments apply to the people added in this call.
  • name renames the share. expiresAt is a date (YYYY-MM-DD, today or later) that sets the expiry, or null to 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 matches createRecordShare.

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:

  • templateName is 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.
  • visibility defaults to "private", which needs at least one user, email or group. "public" gives an open link and must not name anyone.
  • childForms lists child types the submitter fills in together with the form: templateName, and optionally min (default 0) and max (default 1).
  • submissionGrouping is "NONE" (the default), "DAY", "WEEK", "MONTH" or "YEAR", and files submissions into a new folder for each period. submissionNotifications turns the email to the sharer on each submission on or off; it defaults to true.
  • expiresAt is 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 }. url is 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: null removes the expiry, and childForms replaces 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:

  • adoid and text are required. text cannot be empty.
  • Omit cell to comment on the record itself. A record's own thread and each cell's thread are separate, so read the thread you mean with getComments if 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) where refPath can 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?) where options can include { recursive: true } to also update all descendants recursively
  • record.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 await these methods. Each returns the refreshed record after the property update.
  • lockCell and unlockCell change editability through CELL_LOCKED; they do not change visibility.
  • hideCell and unhideCell change visibility through CELL_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​

  • input is action-driven payload (for example custom fields and upstream mapped values)
  • input.recordId may 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.recordId is the host record being recomputed
  • input.changedCells is 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.id
  • record.name
  • record.url
  • record.cellValues (field key/position values)
  • record.meta with only these fields:
    • parent
    • assignees
    • name
    • templateID
    • templateName
    • templateVersion
    • locked
    • created
    • createdby
    • updated
    • updatedby

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(...), or output.html(...).
  • All anydb.* data APIs are async. Always await returned values before using them.
  • For criteria-based queries, prefer anydb.findRecords(...) or anydb.findRecordsPage(...).
  • For full type scans without filters, anydb.getRecordsByType(...) is acceptable.
  • For findRecords and findRecordsPage, use exactly one type selector: type OR typeName OR templateName.
  • Do not use templateId inside script runtime query params.
  • Use condition with 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(...) and output.set(...).

Record customization Script Policy​

  • Treat Record customization script as update-reactive logic that may run frequently.
  • Use input.changedCells as trigger context and return early when monitored cells are not part of the update.
  • Resolve host context via input.recordId and await anydb.getRecordById(input.recordId).
  • For host-record updates, prefer record.setCell(...) and record.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:

  • to accepts comma-separated AnyDB user emails and/or user IDs.
  • Only resolvable AnyDB users are notified.
  • hyperlink is optional and defaults to empty when omitted.
  • sendEmail is optional and defaults to true: each notified user also gets an email. Set sendEmail: false for in-app only.
  • subject is 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.
  • from is 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, pass record.meta.updatedby so the notification names whoever made the change.
  • pushnotification is optional and defaults to false in scripting runtime; set pushnotification: true to 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:

  • to accepts 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:

  • adoid is the record to generate from. formattedExportAdoid is the Document Generation template: the File record holding the .docx or .xlsx uploaded under Document Generation in the workspace settings.
  • The output is a PDF by default, matching the Document Generation workflow action. Pass asPdf: false to get the document in the template's own format (.docx or .xlsx); it attaches to sendEmail the same way.
  • The workflow's execution user must be able to read both the record and the template.
  • generateDocument returns a document handle (generated-document://…), not a file path. The handle is only valid in the script run that created it; use it with sendEmail or attachDocument in the same run. The document is not saved to any record unless you pass it to attachDocument.
  • If the template is missing or cannot be rendered, generateDocument throws 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:

  • attachments is an array (a comma-separated string also works) of document handles from generateDocument in 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:

  • parent is the record the document is attached to. It can be any record, not only the one the document was generated from.
  • document must be a handle generateDocument returned in the same run. A file path or an existing File record ID is refused.
  • name sets 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, attachDocument throws.
  • The returned ID is a File record, so you can pass it to sendEmail in attachments, or store it in a cell.
  • In a simulate (dry run) nothing is saved. attachDocument returns a simulated-… ID, which sendEmail accepts 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), and for(;;)).
  • Loop statements (for, while, do...while, for...of, for...in) must include at least one await in the loop body.
  • For long-running loops, explicitly yield using await anydb.yield() (or await 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; base is 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:
    • sendEmail never accepts a file path, only document handles from generateDocument in 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.