Skip to content

GraphQL ​

CodeKasten serves a GraphQL endpoint whose schema is built from your own record types. A type called Project gives you project, projectList, projectCount, projectAggregate, createProject, updateProject and deleteProject. Add a field in the Entity Builder and it is in the schema.

POST /graphql
Authorization: Bearer sk_…

The API key decides the workspace, the branch and who you are. Opening /graphql in a browser gives an explorer for your schema.

Who am I ​

graphql
query {
  me { workspaceId branchId role readOnly }
}

role is your role in the workspace. readOnly is true for a key made read only, and for one whose person only reads its branch.

Reading ​

Each record type has three queries:

graphql
query Board($search: String, $filter: ProjectFilter, $sort: ProjectSort) {
  projects: projectList(search: $search, filter: $filter, sort: $sort, limit: 20, offset: 0) {
    id
    name
    createdAt
    projectStatus { id name }
    projectPriority { id name }
  }
  total: projectCount(search: $search, filter: $filter)
}
graphql
query {
  project(id: "PRJ…") {
    name
    summary
    labels { id name }
  }
}
  • A type's fields are the ones its detail view shows (its list view for the items of …List).
  • A select field resolves to the record it points at; a relation to the records at the other end.

Filter, search, sort ​

ArgumentTakes
filterAny field of the type, with eq, ne, in, contains, startsWith, gt, gte, lt, lte or isNull. A select is compared by the id it stores; a number field as a number, anything else as its stored text (dates are stored year first, so they compare in order)
searchWords looked for in the name
sortA field, or created / modified, and a direction
limit, offsetPaging
graphql
query($status: JSON) {
  projectList(filter: { projectStatus: { eq: $status } }, sort: { field: created, direction: DESC }) { id name }
}

Writing ​

graphql
mutation($input: ProjectInput!) {
  createProject(input: $input) { id name projectStatus { name } }
}
json
{ "input": { "name": "Trade fair booth", "projectStatus": "Planned" } }

A select field takes a choice's id or its name. updateProject(id, input) and deleteProject(id) work the same way, and linkRecords / unlinkRecords join two records by a relation.

Mutations exist for the types your workspace may keep records of. Lists an app maintains itself (statuses, priorities) are read only.

The same rules as everywhere ​

A write through GraphQL goes through the same record layer as an agent's, with the same rules: your role in the workspace and on the branch, a read-only key, protected branches, which types are records. So a key never writes to a protected branch here either.

What is exposed ​

Records only: data and the app data installed packages supply. Definitions, settings, credentials, people and Inbox items are not in the schema.

Errors ​

Errors carry a code in their extensions.

CodeMeaning
REFUSEDA rule said no. The message says which, in plain words
NOT_FOUNDNo such record on this branch
RATE_LIMITEDToo many requests; see Rate limits
UNAVAILABLEThe service that performs writes cannot be reached

Counting and adding up ​

projectAggregate answers what a report asks: how many records, or the sum, average, smallest, largest or number of distinct values of a field - over everything, or grouped by up to two things.

graphql
query {
  ticketAggregate(
    where: { and: [{ field: "urgent", op: "eq", value: true }, { field: "due", op: "gte", value: "-30d" }] }
    groupBy: [{ field: "status" }]
    measures: [{ fn: count }, { fn: sum, field: "hoursSpent" }]
  ) {
    rows { groups labels values }
    total
    truncated
  }
}
Argument
whereWhich records, as JSON: a condition { field, op, value }, a relation { relation, has: "any" | "none" | [ids] }, or { and: [...] }, { or: [...] }, { not: {...} } of them. Operators: eq, ne, in, nin, contains, startsWith, gt, gte, lt, lte, between, isNull. A date takes "2026-03-01" or a word: today, -30d, start_of_month
groupByUp to two: a field, a date by a bucket (day, week, month, quarter, year), or a relation by its key
measuresUp to six: count, or sum, avg, min, max, distinct of a field. The count when left out
sort, limit, timezoneBy a group or a measure; 200 groups unless told otherwise, 2000 at most; where days and months are cut

Each row has the group's values as stored (groups: the id a select points at, a period's first day), the same as people read them (labels: the name of the record an id stands for), and the measures in the order asked (values). total is the measures over everything the filter keeps.

Unlike the list, it counts the workspace's own records of the type, not what an installed app supplies. The same question can be asked without GraphQL, at /api/query.

Not there yet ​

Cursor paging, subscriptions, and people as objects (a select that points at a person gives the id).

Every picture on this site was saved by a test that had just seen the feature work.