Run awork API actions from JavaScript. Use the curated awork helpers listed in find_guidance, or awork.get/list/download/post/put/delete with relative awork API routes discovered via find_capability (never guess endpoint paths). Always await these calls; do NOT use fetch, imports, require, or absolute URLs.
Available non-blocking awork API helpers, each resolving to { ok: boolean, statusCode: number, headers: Record<string, string>, body: any }:
- gen_link(entity, id, options?) returns a verified absolute awork link. Agent runtime uses the current workspace URL by default; standalone execution falls back to the generic application URL. Pass { external: true } only for Connect/external projects, tasks, or documents. Supported entities: project, task, document, document-space, company, user, time-report, task-view, project-template, task-template, task-bundle, thread, agent, skill, connector.
- await awork.read(route, query?) — GET returning an object, array, or primitive; the route schema determines the response shape
- await awork.get/list(route, query?) — compatibility aliases for awork.read
- await awork.download(route, query?, options?) — prepare any GET response for out-of-band download; body is metadata { resourceUri, mimeType, fileName? }. Use options.fileName to preserve the downloaded filename; MCP callers receive a short-lived HTTPS downloadUrl without file bytes entering model context.
- await awork.post(route, body?, query?), await awork.put(route, body, query?), await awork.delete(route, query?) — delete and some post calls return NoContent without a body
Additional non-blocking sandbox helpers:
- await sandbox.data(dataRef) loads a stored result payload for filtering, joining, or aggregation in JavaScript.
- In agent runtime, await sandbox.writeData(dataRef, path) writes the stored JSON unchanged to a private scratch/ file for later bash processing. Pass the exact dataRef ID string returned by the earlier call. Do not call sandbox.data first or pass its loaded object. Use writeData for large spreadsheet and document exports instead of returning the rows through model context.
console.log/info/warn/error/debug output is captured with bounds; when the script logs, the result gains logLines plus logRef (logRef is omitted when storing fails). Load captured lines with await sandbox.data(logRef). A non-object result is wrapped as { result, ... } when log metadata is present.
Do not build binary or compressed files in this JavaScript runtime. Browser and Node globals such as Blob, CompressionStream, Response, TextEncoder, Buffer, and btoa are not available.
Always await awork API calls, sandbox.data, and sandbox.writeData.
Read data from response.body. The query argument is already the query object: it is second for reads, download, and delete, and third for post and put. Use awork.list('/tasks', { filterby, orderby, page, pageSize }). Promise.all/allSettled can consume direct or stored calls; do not leave calls unawaited or use Promise.any/race.
Downloads and exports: MCP materializes await awork.download(...) during execution, so its body contains a short-lived HTTPS downloadUrl instead of an opaque resource handle. This is an out-of-band transport URL: the client must download it directly and must not pass it to resources/read. This keeps file bytes out of model context. Use it for binary files and query-dependent file responses such as document Markdown exports. Binary/file routes must use await awork.download so binary is not parsed as JSON.
Data rules: JSON request and response properties use camelCase. Date/time fields use UTC datetime strings such as 2026-05-26T12:34:56Z with second precision. Duration fields are usually seconds. UUID 01010001-0000-0000-0000-111111111111 represents actions taken by awork.
Display rule: return human-readable names, links, counts, and short summaries for users. Include stable awork IDs only when they help identify records unambiguously or when another tool call needs them.
HTML/comment rule: task and project descriptions and comment messages use HTML. Use synchronous awork.markdownToHtml(markdown), which returns HTML without an API call. Preserve existing HTML; never convert it as Markdown. Unconverted Markdown is rejected before writing. Conversion does not add formatting features to the target editor. Comment text can contain escaped sequences such as \u003C and \u003E; decode them when displaying formatted text.
Filtering: ALWAYS use server-side filterBy over in-memory filtering; prefer the most specific endpoint plus filterby/orderby/page/pageSize over pulling large collections, and use local .filter() only for shaping already-filtered results. FILTER RULE: never invent filterby properties — some are filter-only and nested models (project inside a task or timeentry) can differ; call find_guidance with skillIds: ['filter-syntax'] and includeContent: true for syntax and use find_capability to confirm supported fields for the exact endpoint before executing. Endpoint filterby is deterministic and not fuzzy; use GET /search for fuzzy workspace search or narrow reads for stable identifiers. For exact count-only checks, add count: true and read response.headers['aw-totalitems']; count responses always have empty body, so do not read response.body from count requests.
BUDGET RULE: the runtime call budget is limited. Estimate the operation count before .map()/.forEach() writes and split large jobs into small chunks. Avoid n+1 calls: filter server-side and use batch endpoints where they exist (e.g. POST /tasks/delete accepts up to 1000 task ids per request). awork helpers execute sequentially, including inside allowed promise aggregates.
Writes: PUT replaces the whole resource — spread the existing entity and override only the changed fields; missing first-level fields are set to null by the API. Verify ids taken from earlier responses are non-empty before using them in routes or bodies (cross-operation refs like projectResponse.body.id resolve at execution time). Do not return legacy arrays like [{ method: 'POST', route: '/tasks' }]; they are rejected.
Document rule: always read and write document content as canonical Markdown. Read the complete content through GET /documents/{documentId}/content?format=markdown, preserve its front matter plus Comment Threads and Preserved Rich Content sections, then write the complete Markdown with multipart field `contentFormat=markdown` through PUT /documents/{documentId}/content. Canonical Markdown escapes placeholder braces, for example `{{owner}}` becomes `\{\{owner\}\}`; replace the escaped token. Never retry the same document write in a loop. After a mismatch, make at most one corrective write and one final read. Canonical links such as `[@Name](</users/{id}>)`, `[Task](</tasks/{id}>)`, and `[File](</documents/{documentId}/files/{fileId}>)` restore editor mention/file nodes, but do not send mention notifications. Rich tables use nested ::: awork-table/tr/td/th/p blocks with Markdown content and {colwidth="196" colspan="2"} attributes. Keep matching fences; outer fences are longer. Edit cells in place. Preserved Rich Content is for unknown nodes and older exports. Re-read with `format=markdown` to verify the round trip. Use HTML only when the user explicitly requests raw editor HTML or to debug editor rendering. When creating or renaming document metadata, put a decorative emoji only in the `emoji` field and keep `name` free of that leading emoji. Preserve an existing name during unrelated metadata updates.
Result format: returns your final JS expression/object (a compact summary if nothing is returned). Large payloads are truncated with dataRef for continue_reading; set forceDataRef=true when another MCP client step needs the full result — the client can then write rows to JSON, CSV, XLSX, or another local artifact format with its own tools. On partial failure, the result includes succeededOperationRefs/failedOperationRefs with operation indexes and extracted resourceId where available; use these IDs before retrying to avoid duplicate creates.
Guidance: call find_guidance with skillIds: ['task-management'] before task mutations, skillIds: ['project-management'] before project mutations, skillIds: ['entity-resolution'] before resolving ambiguous user-provided entity names, and skillIds: ['filter-syntax'] before non-trivial filters. Keep allowNonSuccessStatusCodes=false unless you intentionally handle non-success HTTP status codes. Use continue_reading for JMESPath filtering/slicing after execution.
Destructive rule: require approval for delete/trash/remove, permission or security changes, and bulk/mass mutations. Routine reversible writes affecting one entity — such as create, rename, status, date, assignment, or description updates — do not require approval. Before an approval-required action, preflight a normal read with the same final filter to verify the affected ids, and run a separate count-only request when an exact total is needed.
Example count-only read:
const check = await awork.list('/timeentries', { filterby: 'duration gt 0', count: true });
return { total: check.headers['aw-totalitems'] };
Example paginated batch write (server-side filter, batch endpoint, loop until done):
let deletedCount = 0;
while (true) {
const tasksResponse = await awork.list('/projects/{id}/projecttasks', { filterby: "taskStatus/type eq 'done'", page: 1, pageSize: 1000 });
if (tasksResponse.body.length === 0) { break; }
await awork.post('/tasks/delete', { taskIds: tasksResponse.body.map(t => t.id) });
deletedCount += tasksResponse.body.length;
}
return { deletedCount };
Example update from read results:
const tasksResponse = await awork.list('/projects/{projectId}/projecttasks', { page: 1, pageSize: 50 });
for (const task of tasksResponse.body) {
await awork.put('/tasks/' + task.id, {
...task, // PUT replaces the whole resource: keep all existing first-level properties
dueOn: '2026-04-01'
});
}
return { updated: tasksResponse.body.length };
awork_action