TypeScript Examples
The examples below form one sequence — the same operations appear in cURL, Python and TypeScript — so you can switch languages without switching mental models. Set up authentication first.
Setup
const BASE_URL = process.env.AGILITY_BASE_URL!;
const HEADERS = {
Authorization: `Bearer ${process.env.AGILITY_ACCESS_TOKEN!}`,
'Content-Type': 'application/json',
Accept: 'application/json',
};
type Asset = { _oid: string } & Record<string, unknown>;
async function query(payload: unknown): Promise<Asset[]> {
const response = await fetch(`${BASE_URL}/query.v1`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify(payload),
});
if (!response.ok) throw new Error(`query.v1 failed: ${response.status}`);
const [results] = (await response.json()) as Asset[][];
return results;
}
Who Am I?
The best connection test — it proves the URL and the credentials, and tells you which member the token acts as:
const [me] = await query({
from: 'Member',
where: { IsSelf: true },
select: ['Name', 'Nickname', 'Email'],
});
console.log(`Authenticated as ${me.Name} (${me._oid})`);
query.v1 returns an array of result arrays — one inner array per query
in the request. There is no results or query wrapper object:
[
[
{ "_oid": "Story:1207", "Number": "S-01001", "Name": "Logon" }
]
]
Get One Story by Number
const [story] = await query({
from: 'Story',
where: { Number: 'S-01001' },
select: ['Number', 'Name', 'Description', 'Status.Name'],
});
List a Backlog, Paged Correctly
Page until a response comes back shorter than the page size — no total
needed. Scope.ParentMeAndUp means "in this project or any project beneath
it":
async function* backlog(scopeOid: string, size = 50): AsyncGenerator<Asset> {
for (let start = 0; ; start += size) {
const page = await query({
from: 'PrimaryWorkitem',
filter: [`Scope.ParentMeAndUp='${scopeOid}'`],
select: ['Number', 'Name', 'Status.Name'],
sort: ['Number'],
page: { size, start },
});
yield* page;
if (page.length < size) return;
}
}
for await (const item of backlog('Scope:0')) {
console.log(item.Number, item.Name);
}
Filter Precisely
Quote filter values, compare relations by OID, and prefer RollupState
and UTC dates for queries that survive renames and timezones:
const active = await query({
from: 'Story',
where: { Scope: 'Scope:0' },
filter: ["Estimate>'2'", "Status.RollupState='Active'"],
select: ['Number', 'Name', 'Estimate', 'Status.Name'],
sort: ['-ChangeDateUTC'],
page: { size: 50, start: 0 },
});
Traverse Relations
Dotted paths flatten into their own keys; a nested subquery groups child attributes into objects:
const [detail] = await query({
from: 'Story',
where: { Number: 'S-01001' },
select: [
'Name', 'Scope.Name', 'Owners.Name',
{ from: 'Children:Task', select: ['Number', 'Name', 'Status.Name'] },
],
});
console.log(detail['Scope.Name']); // flattened key
for (const task of detail['Children:Task'] as Asset[]) {
console.log(' ', task.Number, task.Name); // grouped objects
}
Create and Update with the Bulk API
One surface for writes; the response lists what was created, modified and what failed:
async function bulk(commands: unknown[]): Promise<any> {
const response = await fetch(`${BASE_URL}/api/asset`, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify(commands),
});
if (!response.ok) throw new Error(`api/asset failed: ${response.status}`);
return response.json();
}
const created = await bulk([{ AssetType: 'Story', Name: 'My new story', Scope: 'Scope:0' }]);
const storyOid = created.assetsCreated.oidTokens[0].split(':', 2).join(':'); // strip the moment
await bulk([{ from: storyOid, update: { Estimate: 3, Owners: { add: ['Member:20'] } } }]);
Several commands can share one request, and later commands can reference earlier ones by alias:
await bulk([
{ '@epic': { AssetType: 'Epic', Name: 'New initiative', Scope: 'Scope:0' } },
{ '@story': { AssetType: 'Story', Name: 'First slice', Scope: 'Scope:0', Super: '@epic' } },
]);
Run an Operation
Lifecycle changes are operations on rest-1.v1 — query parameter, empty
body:
await fetch(`${BASE_URL}/rest-1.v1/Data/Story/3884?op=Inactivate`, {
method: 'POST',
headers: { Authorization: HEADERS.Authorization },
});
Handle Failures Properly
A Bulk API request answers 200 even when commands fail — always check
commandFailures, and resolve violation tags to human-readable text
through the localizer (unmatched tags echo back unchanged):
const result = await bulk([{ from: 'Story:3884', update: { Name: 'renamed' } }]);
if (result.commandFailures.count > 0) {
const tag: string = result.commandFailures.commands[0].error.message;
const response = await fetch(`${BASE_URL}/loc-2.v1?[${encodeURIComponent(tag)}]`, {
headers: { Authorization: HEADERS.Authorization },
});
const readable = await response.json();
console.error(readable[tag] ?? tag);
}
Related Topics
- Query size and performance — paging and projection discipline
- Bulk API — the full command reference
- Error conventions — including localizable violation tags