Recipes
Complete, copy-ready examples for the jobs people most often build with the API. Each one is short enough to read in a minute and adapt to your own tools.
A helper to start from
The JavaScript recipes use this small helper. It sends a query with its variables and returns data, or throws with the API's error messages. It needs Node 18 or later, with your key in the SUCCESS_API_KEY environment variable.
const API_URL = 'https://www.success.co/graphql'
async function gql(query, variables = {}) {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SUCCESS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
})
const body = await response.json()
if (!response.ok || body.errors || body.error) {
const messages = body.errors?.map((e) => e.message) ?? [body.error ?? response.statusText]
throw new Error(`Success.co API: ${messages.join('; ')}`)
}
return body.data
}
Send a Scorecard number every week
Push a number from another system (revenue from your accounting tool, tickets closed from your help desk) into a weekly measurable. Run it on a schedule, such as every Monday morning; running it twice in a week just updates the number.
It does four things:
- Finds the measurable by name.
- Works out the Monday that starts the week, which is the
startDateevery weekly value needs. - Looks for a value already in that week, including a deleted one, as there can only be one per week.
- Updates that value, or creates one if there isn't one.
// Monday of the week that contains `date`, as YYYY-MM-DD (UTC)
function mondayOf(date = new Date()) {
const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()))
const daysSinceMonday = (d.getUTCDay() + 6) % 7 // Monday 0 … Sunday 6
d.setUTCDate(d.getUTCDate() - daysSinceMonday)
return d.toISOString().slice(0, 10)
}
async function sendWeeklyNumber(measurableName, value, date = new Date()) {
const { dataFields, currentCompanyId } = await gql(
`query ($name: String!) {
currentCompanyId
dataFields(filter: {
name: { equalToInsensitive: $name }
type: { equalTo: "WEEKLY" }
stateId: { equalTo: "ACTIVE" }
dataFieldStatusId: { equalTo: "ACTIVE" }
}) { nodes { id name } }
}`,
{ name: measurableName },
)
const measurable = dataFields.nodes[0]
if (!measurable) throw new Error(`No active weekly measurable called "${measurableName}"`)
const startDate = mondayOf(date)
const { dataValues } = await gql(
`query ($dataFieldId: UUID!, $startDate: Date!) {
dataValues(filter: { dataFieldId: { equalTo: $dataFieldId }, startDate: { equalTo: $startDate } }) {
nodes { id }
}
}`,
{ dataFieldId: measurable.id, startDate },
)
const existing = dataValues.nodes[0]
if (existing) {
// Also brings back a value someone deleted in that week
await gql(
`mutation ($id: UUID!, $value: String!) {
updateDataValue(input: { id: $id, patch: { value: $value, stateId: "ACTIVE" } }) { dataValue { id } }
}`,
{ id: existing.id, value: String(value) },
)
} else {
await gql(
`mutation ($dataValue: DataValueInput!) {
createDataValue(input: { dataValue: $dataValue }) { dataValue { id } }
}`,
{ dataValue: { dataFieldId: measurable.id, startDate, value: String(value), companyId: currentCompanyId } },
)
}
console.log(`${measurable.name}: ${value} for the week of ${startDate}`)
}
await sendWeeklyNumber('Weekly revenue', 48250)
For monthly measurables the startDate is the 1st of the month, and for annual ones 1 January. Quarterly measurables use your company's quarter start dates, which you can read with company(id: "...") { quarterOneDate quarterTwoDate quarterThreeDate quarterFourDate }.
Create an Issue from another tool
Raise an Issue on a team's list when something happens elsewhere, such as an urgent support ticket or a failed payment. This finds the team by name and the owner by email, then creates the Issue on the short-term list.
async function raiseIssue({ teamName, ownerEmail, title, details = '' }) {
const { teams, users, currentCompanyId } = await gql(
`query ($team: String!, $email: String!) {
currentCompanyId
teams(filter: { name: { equalToInsensitive: $team }, stateId: { equalTo: "ACTIVE" } }) { nodes { id } }
users(filter: { email: { equalToInsensitive: $email }, stateId: { equalTo: "ACTIVE" } }) { nodes { id } }
}`,
{ team: teamName, email: ownerEmail },
)
if (!teams.nodes[0]) throw new Error(`No team called "${teamName}"`)
if (!users.nodes[0]) throw new Error(`No user with the email ${ownerEmail}`)
const { createIssue } = await gql(
`mutation ($issue: IssueInput!) {
createIssue(input: { issue: $issue }) { issue { id number } }
}`,
{
issue: {
name: title,
desc: details ? `<p>${escapeHtml(details)}</p>` : '',
teamId: teams.nodes[0].id,
userId: users.nodes[0].id,
type: 'short-term',
companyId: currentCompanyId,
},
},
)
return createIssue.issue
}
function escapeHtml(text) {
return text.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c])
}
await raiseIssue({
teamName: 'Leadership Team',
ownerEmail: 'sam@example.com',
title: 'Three enterprise renewals at risk this month',
details: 'Flagged by the CRM health check.',
})
The key's owner needs access to that team, just as in the app; if they don't have it, the API refuses the change (see Errors and limits). To create To-dos instead, the same pattern works with createTodo; see Data model for its required fields.
Export a team's Rocks
Pull a team's current Rocks, with their owners, statuses and Milestones, into a CSV file for a board pack or a BI tool. This one is in Python and uses the requests library.
import csv
import os
import requests
API_URL = "https://www.success.co/graphql"
HEADERS = {"Authorization": f"Bearer {os.environ['SUCCESS_API_KEY']}"}
def gql(query, variables=None):
body = requests.post(API_URL, headers=HEADERS, json={"query": query, "variables": variables or {}}).json()
if "errors" in body or "error" in body:
raise RuntimeError(body.get("errors") or body.get("error"))
return body["data"]
team = gql(
"""query ($name: String!) {
teams(filter: { name: { equalToInsensitive: $name }, stateId: { equalTo: "ACTIVE" } }) { nodes { id } }
}""",
{"name": "Leadership Team"},
)["teams"]["nodes"][0]
links = gql(
"""query ($teamId: UUID!) {
teamsOnRocks(filter: { teamId: { equalTo: $teamId }, stateId: { equalTo: "ACTIVE" } }) { nodes { rockId } }
}""",
{"teamId": team["id"]},
)["teamsOnRocks"]["nodes"]
rocks = gql(
"""query ($ids: [UUID!]) {
rocks(
filter: { id: { in: $ids }, stateId: { equalTo: "ACTIVE" }, isArchived: { equalTo: false } }
orderBy: DUE_DATE_ASC
) {
nodes {
name
dueDate
rockStatus { name }
user { firstName lastName }
milestones(filter: { stateId: { equalTo: "ACTIVE" } }) { nodes { milestoneStatusId } }
}
}
}""",
{"ids": [link["rockId"] for link in links]},
)["rocks"]["nodes"]
with open("rocks.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Rock", "Owner", "Due", "Status", "Milestones done"])
for rock in rocks:
milestones = rock["milestones"]["nodes"]
done = sum(1 for m in milestones if m["milestoneStatusId"] == "COMPLETE")
owner = f"{rock['user']['firstName']} {rock['user']['lastName']}"
writer.writerow([rock["name"], owner, rock["dueDate"], rock["rockStatus"]["name"], f"{done} of {len(milestones)}"])
print(f"Wrote {len(rocks)} Rocks to rocks.csv")
Pick up changes since your last run
To keep another system in step with Success.co (a data warehouse, a reporting database), fetch only what changed since you last looked, rather than everything each time. Every item's updatedAt moves forward whenever it's created, changed or deleted, so filter on it and keep the latest time you've seen.
async function changedIssuesSince(since) {
const changed = []
let after = null
do {
const { issues } = await gql(
`query ($since: Datetime!, $after: Cursor) {
issues(
first: 200
after: $after
filter: { updatedAt: { greaterThan: $since } }
orderBy: [UPDATED_AT_ASC, ID_ASC]
) {
pageInfo { hasNextPage endCursor }
nodes { id number name issueStatusId teamId stateId updatedAt }
}
}`,
{ since, after },
)
changed.push(...issues.nodes)
after = issues.pageInfo.hasNextPage ? issues.pageInfo.endCursor : null
} while (after)
return changed
}
const lastRun = '2026-11-01T00:00:00Z' // load this from wherever you saved it last time
const changes = await changedIssuesSince(lastRun)
for (const issue of changes) {
if (issue.stateId === 'DELETED') {
// remove it from your copy
} else {
// insert or update it in your copy
}
}
const nextRun = changes.at(-1)?.updatedAt ?? lastRun // save this for next time
Don't filter on stateId here: deleted items are changes too, and this is how you hear about them. Run one of these per object you copy (todos, rocks, headlines, dataValues and so on).
List overdue To-dos
The basis for a weekly reminder or a digest: every open To-do past its due date, grouped by owner.
query Overdue($today: Date!) {
todos(
filter: {
stateId: { equalTo: "ACTIVE" }
todoStatusId: { equalTo: "TODO" }
dueDate: { lessThan: $today }
}
orderBy: [USER_ID_ASC, DUE_DATE_ASC]
) {
totalCount
nodes {
name
dueDate
team { name }
user { firstName lastName email }
}
}
}
Pass today's date as today, for example { "today": "2026-11-12" }.