AnyDB Scripting
This page documents the reusable scripting runtime used by AnyDB script features, including Automation Script Action and Script Cell.
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, hyperlink?, pushnotification? })sends in-app user notifications (supports comma-separated AnyDB user emails/user IDs). Mobile push is opt-in withpushnotification: true.anydb.sendEmail({ to, subject, body })sends email and raises in-app notification for resolvable AnyDB users intoanydb.createRecord({ name, parentid?, typeid?, typename?, cellValues? })anydb.updateRecord({ adoid, cellValues? })anydb.yield()(cooperative event-loop yield for long-running loops)output.summary(text)output.cellValue(value)(primarily for Script Cell return value)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, createRecord, updateRecord, yield
- Output APIs: summary, cellValue, markdown, text, json, table, csv, set
- Utilities: fetch, yieldNow, console.log / log
- Record helpers: getCell, getRefCellValue, setCell, setCellProps, setCellRefValue, lock, unlock, lockCell, unlockCell
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 to AnyDB users by email or user ID; mobile push is opt-in.
// Signature: anydb.sendNotification({ to, message, hyperlink?, pushnotification? })
const notify = await anydb.sendNotification({
to: "owner@company.com,67f1abc234def56789012345",
message: "Invoice requires 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 })
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);
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 one existing record by ADO ID.
// Signature: anydb.updateRecord({ adoid, cellValues? })
const updated = await anydb.updateRecord({
adoid: "67f1abc234def56789012345",
cellValues: {
Status: "Ready",
},
});
log(`Updated: ${updated.id}`);
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.cellValue
Purpose: Set the Script Cell computed value.
// Signature: output.cellValue(value)
// Typical for Script Cell scripts
output.cellValue("OK");
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.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.
// 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. For lock/unlock behavior only, prefer record.lockCell(...) and record.unlockCell(...).
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. 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. 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");
}
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
Script Cell
input.recordIdis the host record being recomputedinput.changedCellsis available to identify what changed on this update- Script Cell runs on record updates, so always guard early and return quickly when not relevant
- Use
output.cellValue(...)to set the script cell value
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: Script Cell
// 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(...).
Script Cell Policy
- Treat Script Cell code 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(...). - Use
output.cellValue(...)only when the script should set or refresh the Script Cell value. - Do not use
output.set(...),output.summary(...),output.markdown(...),output.text(...),output.json(...),output.table(...), oroutput.csv(...)in Script Cell mode. - Keep Script Cell 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.pushnotificationis optional and defaults tofalsein scripting runtime; setpushnotification: trueto explicitly send mobile push.
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 }.
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.