Developer reference

The Noded Graph API

Noded keeps a living graph of your customers: the people and companies you work with, everything that happens with them, and what Noded has learned along the way. The Graph API is the GraphQL API that powers every Noded surface - the web app, the SDK, and the MCP integrations all use it. This page explains how the API works and documents every operation you can call, each with a real request and response.

Making requests

The Graph API lives at a single endpoint. You send it a GraphQL document and a JSON object of variables, and it sends back JSON. Every request needs a bearer token in the Authorization header.

If you use the Noded SDK, you do not need to think about any of this. The typed helpers (noded.people.search, noded.notes.create, and friends) cover the common cases, and noded.graphql(query, variables) runs any operation on this page with auth and headers handled for you. Reach for raw GraphQL when you need a field or an operation the helpers do not cover.

Three optional headers make the API work better for your users. x-user-timezone tells Noded which timezone to use when it reasons about dates - things like task due dates and "today" boundaries. x-user-locale sets the language. x-user-client-version names your app, which helps us find your traffic if you ever ask for help.

You can batch requests by sending an array of operations in one POST. Introspection is turned off, so a GraphQL explorer cannot discover the schema on its own - this reference and the published schema file are the source of truth.

At a glance

EndpointPOST https://api.getnoded.ai/api/v1/graph
AuthAuthorization: Bearer <token>
BodyJSON: { "query": "...", "variables": { ... } }
RealtimeGraphQL over WebSocket, same host - see Realtime
Requestcurl
curl -s https://api.getnoded.ai/api/v1/graph \
  -H "Authorization: Bearer $NODED_TOKEN" \
  -H "Content-Type: application/json" \
  -H "x-user-timezone: America/New_York" \
  -H "x-user-client-version: my-app/1.2.0" \
  -d '{
    "query": "query { tenant { _id email tagId } }"
  }'
RequestSDK
import { Noded } from '@bigfootai/noded-sdk';

const noded = new Noded({
  auth: { mode: 'oidc', issuer, clientId, audience },
});
await noded.connect();

const data = await noded.graphql(
  `query { tenant { _id email tagId } }`
);
Response
{
  "data": {
    "tenant": {
      "_id": "6f2a9c1e-8b3d-4e5f-9a7b-1c2d3e4f5a6b",
      "email": "you@yourcompany.com",
      "tagId": "c2d4e6f8-0a1b-4c3d-9e8f-a7b6c5d4e3f2"
    }
  }
}

Errors

The API reports problems in the standard GraphQL way: the response contains an errors array, and each error has a message and a machine-readable extensions.code. Internal details and stack traces are stripped before anything reaches you.

Auth problems are the one case that also changes the HTTP status. A missing, expired, or invalid token returns HTTP 401 with one of the codes shown on the right. Everything else - a bad argument, a record you cannot see, a validation failure - comes back as HTTP 200 with the errors array filled in. So always check errors, even when the status code looks fine.

A good retry policy, and the one the Noded app itself uses: when you get a 401, refresh your token and replay the request once. If it fails again, send the user back through sign-in. Do not retry other errors automatically.

Error codes

CodeMeaning
UNAUTHENTICATED_NO_TOKENNo bearer token was sent. HTTP 401.
UNAUTHENTICATED_TOKEN_EXPIREDThe token has expired. Refresh it and retry once. HTTP 401.
UNAUTHENTICATED_TOKEN_INVALIDThe token could not be validated. HTTP 401.
FORBIDDENThe token is valid, but this action is not allowed.
Responseexpired token
{
  "errors": [
    {
      "message": "Token expired",
      "extensions": {
        "code": "UNAUTHENTICATED_TOKEN_EXPIRED"
      }
    }
  ]
}
Responsenot found
{
  "data": { "block": null },
  "errors": [
    {
      "message": "A block could not be found",
      "path": ["block"]
    }
  ]
}

Authentication

Every request runs as a specific Noded user, called a tenant. The API works out who you are from your bearer token, and it only ever returns data that user could see in the Noded app. There is no anonymous access, and there is no way to read another user's private data.

This has a convenient side effect: you never pass an account or workspace ID. When you see an ID in a filter on this page, it is a tag ID (a person or company in the graph), never an account identifier.

There are two ways to get a token:

  • Sign-in with Noded (recommended for apps with a UI). Noded provisions an OIDC issuer, clientId, and audience for your app - request developer access to get yours. The SDK's noded.connect() opens the Noded login and refreshes tokens automatically. Any standard OIDC client works too.
  • Server-to-server. Backend credentials are provisioned per integration by the Noded team. Contact us to set this up. Never put a long-lived credential in browser code.

One ID is worth fetching right away: your own tag ID. Every Noded user has a person tag that represents them in the graph, and its ID (tenant.tagId) is what you use for filters like "tasks assigned to me" or "notes shared with me".

Requestbrowser sign-in
const noded = new Noded({
  auth: {
    mode: 'oidc',
    issuer: NODED_ISSUER,       // provided by Noded
    clientId: NODED_CLIENT_ID,  // provided by Noded
    audience: NODED_AUDIENCE,   // provided by Noded
  },
});

// Opens the Noded login, then auto-refreshes.
await noded.connect();
Requestwho am I?
query Me {
  tenant {
    _id
    email
    givenName
    tagId   # your own person tag - keep this
  }
}

Concept: tags are the nodes

A tag is an entity in the graph - a person, a company, a topic, a date, or a place. The two kinds you will use most are people and accounts, and both are tags:

  • A person has tagType: "person" and usually an email.
  • An account (a company) has tagType: "topic" with tagSubType: "organization", and usually an emailDomain and a url.

A tag carries identity fields (alias is the display name, plus email, emailDomain, avatar, favicon, url, description), links to other systems (businessObjects point at CRM objects, externalIdLinks point at identities in connected tools), and live data (taskCount, signals).

Everything else in the API hangs off tags. Content is attached to tags. Access is granted through tags. Memory is stored per tag. When in doubt, start by finding the right tag.

ShapeTag (common fields)
type Tag {
  _id: ID!
  alias: String!        # display name
  friendlyName: String
  tagType: TagType!     # person | topic | date | location
  tagSubType: String    # e.g. "organization"
  email: String
  emailDomain: String
  avatar: String
  favicon: String
  url: String
  description: String
  externalId: String
  favorite: Boolean
  archived: Boolean!
  taskCount: Int
  businessObjects: [BusinessObjectLink]
  externalIdLinks: [ExternalIdLink]
}

Concept: blocks are the content

A block is one unit of content on a customer's timeline. Notes, tasks, emails, calendar events, meeting transcriptions, chat messages, documents, and websites are all blocks. In GraphQL terms, Block is an interface, and each kind of content is a concrete type that implements it: Note, Task, Email, Event, Message, Thread, Transcription, Document, Website, Record, Table, and a few more.

Every block shares a common set of fields: blockType tells you what kind it is, title / summary / text give you readable content, and the date fields tell you when it was created, updated, and (for tasks) due. Fields that only exist on one kind - like a task's status or an email's subject - are selected with an inline fragment, as the example shows.

Blocks that came from an outside system (everything except notes) also carry provenance: provider and application say where it came from, and externalId and uri point back at the original.

Keep lists light. A few fields are very large: Email.cleanBodyHtml and a transcription's transcript, highlights, and outline can each be hundreds of kilobytes. Select them only when you fetch a single block, never in a list.

Requestinterface + fragments
query Activity($input: GraphSearchInput) {
  searchGraph(input: $input) {
    _id
    blockType
    title
    summary
    dateCreated
    ... on Task { status dateDue }
    ... on Email { subject direction }
    ... on Event { dateStart dateEnd }
  }
}

Concept: sharing tags are the edges - and the permissions

A sharing tag is the link between a tag and a piece of content. When a note is "about" Acme Robotics, that is a sharing tag pointing at the Acme tag. This one structure does three jobs, and understanding it is the key to the whole API.

First, organization. A block linked to a tag shows up on that tag's timeline. This is how content finds its way onto a person or account page.

Second, access control. The shared flag decides whether the link grants access. With shared: false, the link only organizes - the block appears on the tag's page for people who can already see it. With shared: true, the link grants access to the people behind that tag, at the level set by sharingLevel. This is the only permission system in Noded - there is no separate ACL to manage.

Third, workflow. pinned pins the block to the tag's page, and assigned marks a task as assigned to that person.

Two numbers matter here, and they are plain integers in the schema. sharingLevel: 0 is read-only, 1 is editor, 2 is owner. sharingApproach records how the link was made: 0 inline (typed in the editor), 1 explicit (deliberately added - use this one), 2 system.

Every read you make is filtered by this model automatically. You can also ask any block or folder for your own effective permission through its access field: RW, READ_ONLY, or NO_ACCESS.

ShapeSharingTag
type SharingTag {
  tagId: String!        # the tag this links to
  tag: Tag
  shared: Boolean!      # true = grants access
  sharingLevel: Int!    # 0 read-only, 1 editor, 2 owner
  sharingApproach: Int! # 0 inline, 1 explicit, 2 system
  pinned: Boolean
  assigned: Boolean
}
Exampletwo common shapes
// Tag a note to an account (organize only):
{ "tagId": "<accountTagId>", "shared": false,
  "sharingLevel": 0, "sharingApproach": 1 }

// Share a note with a person as an editor:
{ "tagId": "<personTagId>", "shared": true,
  "sharingLevel": 1, "sharingApproach": 1 }

Concept: tables, records, and signals

Alongside free-form content, the graph holds structured data - usually synced from a CRM or another connected system.

A table is a dataset. Its metadata.fields describe the columns (each with a name, a human label, and a fieldType), and its records are the rows. A record is one row: a list of values, each pairing a field name with a value. When Noded's AI derived a value, the record also carries its reasoning.

A record type is a writing contract on top of a table. It says which operations your integration may perform - check operations { create read update } before you try to write - and which fields each operation accepts.

A signal lifts one field value out of a table and attaches it to a tag, so the important numbers (a renewal date, a health score) are available right where you look at the account.

Examplea record, in practice
{
  "_id": "d8e9f0a1-2b3c-4d5e-8f9a-0b1c2d3e4f5a",
  "values": [
    { "name": "account_name", "value": "Acme Robotics" },
    { "name": "renewal_date", "value": "2027-01-15" },
    { "name": "health_score", "value": "72",
      "reasoning": "Usage is steady but two support escalations are open." }
  ]
}

Concept: the feed and memory

Two more ideas round out the model.

A recommendation is one item in the user's Noded feed: a note Noded drafted, a change it detected, a suggestion to connect a tool, a sync warning. Every recommendation carries ready-to-render display fields (displayTitle, displayDescription, displayLabel) and navigation hints that tell you where it points. Depending on its recommendationType, exactly one of its detail objects is filled in - upsertRecommendation for a drafted note, syncPausedRecommendation for a sync warning, and so on. Render the display fields and you rarely need the details.

Memory is what the graph has learned about a person or account: a list of topic-and-value entries such as "communication style" → "prefers short emails, responds after 5pm". Each entry carries a confidence and a source. Memory is retrievable per tag or all at once, and your integration can write entries of its own.

Examplea memory entry
{
  "topic": "communication style",
  "value": "Prefers short emails; responds after 5pm.",
  "confidence": "high",
  "source": "email",
  "dateUpdated": 1756900000000
}

Conventions

A few rules hold everywhere in the API. Learn them once and every operation on this page will feel familiar.

IDs are strings, and the field is _id. Every object's identifier is a UUID string named _id, not id. The one exception is the small ReferenceBlockInput object used to point at a block from an input - it uses { id, blockType }.

Timestamps are epoch milliseconds. Every date field is a number like 1757083380000 - the number of milliseconds since January 1, 1970. This is true for values the API returns and for dates you pass into filters. In JavaScript, Date.now() and new Date(value) speak this format natively.

Pagination uses size and page. size is how many results you want; page is which page, starting at 0. Two things to know: always send size when you send page (a page number without a size is ignored), and list responses do not include a total - use searchGraphCount or searchTagsByField when you need one. There are no cursors.

Sorting. Graph searches take sort: [{ field: "dateCreated", order: DESC }]. Tag search instead uses named arguments like orderByDateUpdated: "desc".

Use lowercase enum values. blockType, tagType, and status enums accept both cases for historical reasons. Write the lowercase form: note, task, person, topic, completed.

Rich text lives in document. A block's document field holds the full rich-text content in Noded's editor format (serialized ProseMirror JSON). For display, prefer text and summary, which are plain-text versions. Only write document if you produce the same format - the SDK's notes.create does this for you.

Nothing is deleted; things are archived. The archiveOne* mutations set archived: true, and most list operations skip archived items by default. You can pass archive: false to un-archive.

Referencefilter building blocks
# Date ranges - epoch milliseconds
input NumberFilterInput {
  gt: Float
  gte: Float
  eq: Float
  lte: Float
  lt: Float
}

# String matching
input StringFilterInput {
  eq: String
  contains: String
  startsWith: String
  endsWith: String
}

# Sorting
input SortOptionInput {
  field: String          # e.g. "dateCreated"
  order: OrderDirection  # ASC | DESC
}
Examplelast 30 days, newest first
{
  "input": {
    "dateCreated": { "gte": 1754491380000 },
    "sort": [{ "field": "dateCreated", "order": "DESC" }],
    "size": 25,
    "page": 0
  }
}

Tags: people and accounts

These operations find, read, and manage the people and companies in the graph. Start here - almost every other call takes a tag ID that these return.

searchTags

query

Finds tags that match your filters. This is the main way to look up people and accounts. You can search by name, filter by type, or look someone up by email - and you can combine filters freely.

When you pass search, results come back ranked by relevance, so the best match is first. Without search, use one of the orderBy* arguments to control the order.

Arguments (SearchTagsInput)

searchString

Free text to match against names and emails. Results are ranked by relevance.

tagTypeString

Limit to one kind of tag: "person" or "topic". Use tagTypes to pass several.

tagSubTypeString

A finer type. Accounts are tagType: "topic" with tagSubType: "organization".

_ids[String]

Fetch a specific set of tags by ID.

emailString

Find the person tag for one email address.

emailDomains[String]

Find tags whose email domain matches, e.g. ["acmerobotics.com"].

folderIds[String]

Only tags inside these folders.

archivedBoolean

Pass false to hide archived tags. Most apps should.

favorite, hasTasksBoolean

Only favorites, or only tags that have open tasks.

orderByAlias, orderByFriendlyName, orderByTaskCount, orderByDateUpdated, orderByDateCreatedString

Sort direction for that field: "asc" or "desc". Use one at a time.

size, pageInt

Page length and 0-based page number.

Returns

A list of Tag objects. The list is empty when nothing matches - that is not an error.

Request
query Accounts($input: SearchTagsInput) {
  searchTags(input: $input) {
    _id
    alias
    tagType
    tagSubType
    emailDomain
    url
    taskCount
  }
}
Variables
{
  "input": {
    "search": "acme",
    "tagType": "topic",
    "tagSubType": "organization",
    "archived": false,
    "size": 10
  }
}
Response
{
  "data": {
    "searchTags": [
      {
        "_id": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
        "alias": "Acme Robotics",
        "tagType": "topic",
        "tagSubType": "organization",
        "emailDomain": "acmerobotics.com",
        "url": "https://acmerobotics.com",
        "taskCount": 4
      }
    ]
  }
}

tag

query

Fetches one tag. In most cases you pass its _id. You can also look a person tag up by tenantIdVerified - the ID of the Noded user it belongs to - which is how you go from a user to their tag.

If no tag matches, the call returns an error rather than null, so be ready to catch it.

Arguments (FindTagInput)

_idString

The tag's ID. The usual way to call this.

tenantIdVerifiedString

Look up the person tag that belongs to this Noded user.

archivedBoolean

Set to true to allow an archived tag to be returned.

Returns

One Tag. Errors if the tag does not exist or you cannot see it.

Request
query GetAccount($id: String) {
  tag(input: { _id: $id }) {
    _id
    alias
    description
    emailDomain
    url
    favicon
    favorite
    businessObjects { provider url }
  }
}
Response
{
  "data": {
    "tag": {
      "_id": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
      "alias": "Acme Robotics",
      "description": "Industrial robotics maker; mid-market plan since 2025.",
      "emailDomain": "acmerobotics.com",
      "url": "https://acmerobotics.com",
      "favicon": "https://acmerobotics.com/favicon.ico",
      "favorite": true,
      "businessObjects": [
        { "provider": "salesforce",
          "url": "https://acme.my.salesforce.com/001..." }
      ]
    }
  }
}

recentTags

query

Returns the tags the user touched most recently - opened, edited, or had activity on. It takes no arguments. Use it for a "recent" list or to warm up a picker with likely choices.

Returns

A list of Tag objects, most recent first.

Request
query Recent {
  recentTags {
    _id
    alias
    tagType
    avatar
  }
}
Response
{
  "data": {
    "recentTags": [
      { "_id": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
        "alias": "Acme Robotics", "tagType": "topic", "avatar": null },
      { "_id": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
        "alias": "Jane Rivera", "tagType": "person", "avatar": null }
    ]
  }
}

tagActivity

query

Returns a time series of activity for one tag: one entry per block, with its type and date. This powers the activity sparklines in the Noded app. Filter by block type or date range to shape the series.

Arguments (TagActivityInput)

_idStringrequired

The tag to chart.

blockTypes[BlockType]

Only count these kinds of content, e.g. [email, event].

dateCreatedNumberFilterInput

Date range in epoch milliseconds, e.g. { gte: ... }.

Returns

A list of TagActivity points: { _id, blockType, date }.

Request
query Sparkline($input: TagActivityInput!) {
  tagActivity(input: $input) {
    blockType
    date
  }
}
Variables
{
  "input": {
    "_id": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
    "blockTypes": ["email", "event", "note"],
    "dateCreated": { "gte": 1754491380000 }
  }
}
Response
{
  "data": {
    "tagActivity": [
      { "blockType": "email", "date": 1756987200000 },
      { "blockType": "event", "date": 1756900800000 },
      { "blockType": "note",  "date": 1756814400000 }
    ]
  }
}

tagsWithActivity

query

Answers the question "which of my people and accounts need attention right now?" It returns tags along with how many open recommendations each one has. The Noded app uses this for the badge counts in its sidebar.

Arguments (TagsWithActivityInput)

tagIds[String]

Limit to these tags.

folderIds[String]

Limit to tags inside these folders.

tagTypeFilters[TagTypeFilter]

Limit by type, e.g. [{ tagType: "person" }].

limitInt

Maximum number of tags to return.

Returns

A list of { tag, recommendationCount } pairs.

Request
query NeedsAttention {
  tagsWithActivity(input: { limit: 5 }) {
    recommendationCount
    tag { _id alias tagType }
  }
}
Response
{
  "data": {
    "tagsWithActivity": [
      {
        "recommendationCount": 3,
        "tag": { "_id": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
                 "alias": "Acme Robotics", "tagType": "topic" }
      },
      {
        "recommendationCount": 1,
        "tag": { "_id": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
                 "alias": "Jane Rivera", "tagType": "person" }
      }
    ]
  }
}

tagNews

query

Returns recent public news for one or more tags - short snippets with a title, a link, and a publish date. Useful for an account page's "in the news" panel.

Arguments (FindTagNewsInput)

_ids[String!]required

The tags to fetch news for.

Returns

TagNewsOutput: a news list with one entry per tag, each holding its snippets.

Request
query News($input: FindTagNewsInput!) {
  tagNews(input: $input) {
    news {
      _id
      snippets { title snippet url datePublished }
    }
  }
}
Variables
{ "input": { "_ids": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"] } }
Response
{
  "data": {
    "tagNews": {
      "news": [
        {
          "_id": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
          "snippets": [
            {
              "title": "Acme Robotics opens new plant",
              "snippet": "The company announced a second factory...",
              "url": "https://example.com/news/acme-plant",
              "datePublished": 1756814400000
            }
          ]
        }
      ]
    }
  }
}

searchTagsByField

query

A table view over tags. You name the structured fields you want as columns (by provider and field name), and you get each tag together with those field values. You can also filter and sort by a field - "all accounts where health score is below 50, worst first".

This is the only tag operation that returns a total count, which makes it the right choice for a paged table UI.

Arguments (SearchTagsByFieldInput)

fields[FieldReferenceInput!]required

The columns to fetch: { provider, fieldName } pairs.

filters[FieldFilterInput]

Per-field conditions: { provider, fieldName, stringFilter?, numberFilter?, isNull? }.

sortByFieldSortInput

Sort by one field: { provider, fieldName, direction }.

tagTypes, tagSubTypes, tagIds, folderIdslists

Scope which tags appear as rows.

size, pageInt

Page length and 0-based page number.

Returns

TagFieldResults: the rows (tags, each a tag plus its fieldValues), plus totalCount, page, and size.

Request
query AccountsTable($input: SearchTagsByFieldInput!) {
  searchTagsByField(input: $input) {
    totalCount
    tags {
      tag { _id alias }
      fieldValues { fieldName value }
    }
  }
}
Variables
{
  "input": {
    "tagTypes": ["topic"],
    "tagSubTypes": ["organization"],
    "fields": [
      { "provider": "salesforce", "fieldName": "renewal_date" },
      { "provider": "salesforce", "fieldName": "health_score" }
    ],
    "sortBy": { "provider": "salesforce",
                "fieldName": "health_score", "direction": "asc" },
    "size": 25, "page": 0
  }
}
Response
{
  "data": {
    "searchTagsByField": {
      "totalCount": 42,
      "tags": [
        {
          "tag": { "_id": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
                   "alias": "Acme Robotics" },
          "fieldValues": [
            { "fieldName": "renewal_date", "value": "2027-01-15" },
            { "fieldName": "health_score", "value": "72" }
          ]
        }
      ]
    }
  }
}

searchGraphTags

query

Finds tags that are related to a slice of the graph, with statistics about how strongly. Give it a scope - a tag, a folder, a search - and it returns the other tags that show up in the same content, each with an occurrenceCount, a coOccurrencePercentage, and a per-type breakdown of the shared content.

The classic use: "who else is involved with this account?" Scope by the account's tag ID and filter to tagTypes: [person].

Key arguments (GraphSearchTagsInput)

tags[TagFilterItemInput]

The scope: content linked to these tags, e.g. [{ _ids: ["<accountTagId>"] }].

tagTypes, tagSubTypeslists

Which kinds of related tags to return.

minOccurrenceCountInt

Hide tags that appear fewer than this many times.

minCoOccurrencePercentageInt

Hide weak relationships below this percentage.

sizeInt

Maximum results. It also accepts the full set of searchGraph filters to shape the scope.

Returns

A list of GraphTag objects - a slim tag (_id, alias, tagType) plus the relationship stats.

Request
query RelatedPeople($input: GraphSearchTagsInput) {
  searchGraphTags(input: $input) {
    _id
    alias
    tagType
    occurrenceCount
    coOccurrencePercentage
    blockTypes { note email event }
  }
}
Variables
{
  "input": {
    "tags": [{ "_ids": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"] }],
    "tagTypes": ["person"],
    "minOccurrenceCount": 2,
    "size": 10
  }
}
Response
{
  "data": {
    "searchGraphTags": [
      {
        "_id": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
        "alias": "Jane Rivera",
        "tagType": "person",
        "occurrenceCount": 18,
        "coOccurrencePercentage": 64.3,
        "blockTypes": { "note": 5, "email": 9, "event": 4 }
      }
    ]
  }
}

graphTags

query

A simpler version of searchGraphTags for the common case: "show me tags related to this one tag." You pass the tag and the kinds of related tags you want.

Arguments (GraphTagsInput)

tagIdStringrequired

The tag to find relations for.

tagTypes[TagType]required

Which kinds of related tags to return, e.g. [person].

sizeInt

Maximum results.

Returns

A list of GraphTag objects, same shape as searchGraphTags.

Request
query Related {
  graphTags(input: {
    tagId: "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
    tagTypes: [person],
    size: 5
  }) {
    _id
    alias
    relevanceScore
  }
}
Response
{
  "data": {
    "graphTags": [
      { "_id": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
        "alias": "Jane Rivera", "relevanceScore": 0.91 }
    ]
  }
}

searchTagsForBlocks

query

Returns the tags that appear on content matching a graph search. Think of it as "run this searchGraph, then tell me which tags those blocks are linked to." Handy for building filter chips over a list of content.

It takes the same filter vocabulary as searchGraph, plus sharedOnly (only tags whose link grants access) and orderByAlias / orderByTaskCount.

Returns

A list of full Tag objects.

Request
query TagsOnMyTasks {
  searchTagsForBlocks(input: {
    blockTypes: [task],
    orderByTaskCount: "desc",
    size: 20
  }) {
    _id
    alias
    taskCount
  }
}
Response
{
  "data": {
    "searchTagsForBlocks": [
      { "_id": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
        "alias": "Acme Robotics", "taskCount": 4 }
    ]
  }
}

generateKnowledgeGraph

query

Builds a node-and-link graph you can draw. Each node is a tag with counts of its content by type; each link connects two tags that share content. It takes the same scoping input as searchGraphTags.

Returns

KnowledgeGraph: nodes (id, name, tagType, and per-type counts) and links (source and target node ids).

Request
query Graph($input: GraphSearchTagsInput) {
  generateKnowledgeGraph(input: $input) {
    nodes { id name tagType notes emails events }
    links { source target }
  }
}
Response
{
  "data": {
    "generateKnowledgeGraph": {
      "nodes": [
        { "id": "b4f6a2c8-...", "name": "Acme Robotics",
          "tagType": "topic", "notes": 12, "emails": 30, "events": 8 },
        { "id": "e7a9c3b1-...", "name": "Jane Rivera",
          "tagType": "person", "notes": 5, "emails": 22, "events": 6 }
      ],
      "links": [
        { "source": "b4f6a2c8-...", "target": "e7a9c3b1-..." }
      ]
    }
  }
}

upsertOneTag

mutation

Creates a tag, or updates one if you pass an existing _id. Use it to add a person or an account that Noded has not seen yet, or to fix a name, description, or URL.

Only send the fields you want to set. When creating, give at least an alias and a tagType - and an email for a person or a url for an account, so Noded can connect it to incoming activity.

Key arguments (UpsertTagInput)

_idID

Omit to create; pass to update.

aliasString

The display name.

tagType, tagSubTypeString

"person"; or "topic" + "organization" for an account.

emailString

The person's email address.

url, urlsString, [String]

The account's website(s).

description, avatar, favicon, phoneNumber, favoritevarious

Profile details.

businessObjects, externalIdLinkslists

Links to CRM objects and connected-system identities. Prefer the dedicated link mutations for changes.

Returns

The full Tag after the write.

Request
mutation CreatePerson($input: UpsertTagInput) {
  upsertOneTag(input: $input) {
    _id
    alias
    tagType
    email
  }
}
Variables
{
  "input": {
    "alias": "Sam Osei",
    "tagType": "person",
    "email": "sam@acmerobotics.com"
  }
}
Response
{
  "data": {
    "upsertOneTag": {
      "_id": "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5d",
      "alias": "Sam Osei",
      "tagType": "person",
      "email": "sam@acmerobotics.com"
    }
  }
}

archiveOneTag

mutation

Archives a tag, hiding it from lists and search. Nothing is deleted - the tag's content stays in the graph, and you can bring the tag back by calling this again with archive: false.

Arguments (ArchiveTagInput)

_idStringrequired

The tag to archive.

archiveBoolean

true (default) to archive, false to restore.

Returns

{ _id, archived } - the new state.

Request
mutation Archive {
  archiveOneTag(input: {
    _id: "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5d",
    archive: true
  }) {
    _id
    archived
  }
}
Response
{
  "data": {
    "archiveOneTag": {
      "_id": "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5d",
      "archived": true
    }
  }
}

Blocks: activity and content

These operations read and write the content of the graph - notes, tasks, emails, events, meetings, and the rest. The centerpiece is searchGraph, which powers every timeline and list in the Noded app.

searchGraph

query

Searches the graph's content and returns matching blocks. This is the most important read in the API. An account's timeline, a task list, and a full-text search are all just searchGraph with different filters.

All filters combine with AND: if you pass both tags and blockTypes, a block must match both. The one exception is orFilters, which lets you OR several tag filters together.

Always scope your search. If you pass no scoping filter at all, the API does not return the whole graph. It falls back to a default scope: the user's pinned tags plus content they created. That default suits the Noded home feed, but it will surprise you in an integration - so always pass tags, _ids, search, or another scope on purpose.

Arguments (GraphSearchInput) - scope

tags[TagFilterItemInput]

The main scope: content linked to these tags. Each item is { _ids: [tagId], assigned?, pinned?, sharingApproaches? }. Set assigned: true for tasks assigned to that person, or pinned: true for pinned content. (The older top-level tagIds still works, but tags is the current form.)

searchString

Full-text search over content.

_ids[String]

Fetch specific blocks by ID.

folderIds[String]

Content linked to any tag in these folders.

threadIds, uris, parentIdvarious

Scope by thread, by external URI, or by parent.

orFilters{ tags }

OR across several tag filters: match content linked to any of them.

Arguments - type and state

blockTypes[BlockType]

Only these kinds: note, task, email, event, message, transcription, document, website, ...

statuses[TaskStatus]

Task status filter: not_started, in_progress, completed.

favorite, snoozed, template, proposed, generatedBoolean

State flags. generated means Noded's AI created it; proposed means it awaits user approval.

dateCreated, dateUpdated, dateDue, dateAssociated, dateMatchNumberFilterInput

Date ranges in epoch milliseconds: { gt, gte, eq, lte, lt }.

emails[StringFilterInput]

Match against email addresses on the block.

originReferenceBlock, parentReferenceBlock, originReferenceTagIdrefs

Content generated from, or nested under, another block or tag.

Arguments - presets, order, paging

scopeToOwnTasks, scopeToPriorityTasks, scopeToHousekeepingTasksBoolean

Server-side presets matching the task views in the Noded app.

smartBoolean

Let the server order results the way the app's smart lists do (due date first).

sort[SortOptionInput]

e.g. [{ field: "dateCreated", order: DESC }].

size, pageInt

Page length and 0-based page number. Send both together.

Returns

A list of Block objects. Use inline fragments for type-specific fields, and keep list selections light (no email bodies or transcripts).

Request
query AccountActivity($input: GraphSearchInput) {
  searchGraph(input: $input) {
    _id
    blockType
    title
    summary
    dateCreated
    ... on Task { status dateDue }
    ... on Email { subject }
    sharingTags { tagId tag { alias } }
  }
}
Variables
{
  "input": {
    "tags": [{ "_ids": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"] }],
    "blockTypes": ["note", "email", "event", "transcription"],
    "sort": [{ "field": "dateCreated", "order": "DESC" }],
    "size": 20,
    "page": 0
  }
}
Response
{
  "data": {
    "searchGraph": [
      {
        "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
        "blockType": "note",
        "title": "QBR prep - Acme",
        "summary": "Renewal risks and expansion plan for Q1.",
        "dateCreated": 1757000000000,
        "sharingTags": [
          { "tagId": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
            "tag": { "alias": "Acme Robotics" } }
        ]
      },
      {
        "_id": "9b8c7d6e-5f4a-4b3c-8d2e-1f0a9b8c7d6e",
        "blockType": "email",
        "title": "Re: rollout timeline",
        "summary": "Jane confirmed the pilot starts next week.",
        "dateCreated": 1756987200000,
        "subject": "Re: rollout timeline",
        "sharingTags": [
          { "tagId": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
            "tag": { "alias": "Jane Rivera" } }
        ]
      }
    ]
  }
}

searchGraphCount

query

Counts how many blocks match a search, without fetching them. It takes exactly the same input as searchGraph; the server ignores sort, size, and page. Pair it with a paged searchGraph to show "page 1 of 12".

Returns

An integer - the total number of matching blocks.

Request
query HowMany($input: GraphSearchInput) {
  searchGraphCount(input: $input)
}
Variables
{
  "input": {
    "tags": [{ "_ids": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"] }],
    "blockTypes": ["task"],
    "statuses": ["not_started", "in_progress"]
  }
}
Response
{ "data": { "searchGraphCount": 4 } }

block

query

Fetches one block by ID. This is where you ask for the heavy fields you kept out of your lists: the full rich-text document, an email's body, a meeting's transcript.

The access field tells you what the current user may do with this block - useful for showing or hiding an edit button.

Arguments (FindBlockInput)

_idStringrequired

The block's ID.

archivedBoolean

Set to true to allow an archived block to be returned.

Returns

One Block. Errors if it does not exist or you cannot see it.

Request
query GetNote($id: String!) {
  block(input: { _id: $id }) {
    _id
    blockType
    title
    text
    access
    creator { name self }
    dateCreated
    dateUpdated
    sharingTags { tagId shared sharingLevel }
  }
}
Response
{
  "data": {
    "block": {
      "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "blockType": "note",
      "title": "QBR prep - Acme",
      "text": "Renewal risks: support escalations...",
      "access": "RW",
      "creator": { "name": "You", "self": true },
      "dateCreated": 1757000000000,
      "dateUpdated": 1757083380000,
      "sharingTags": [
        { "tagId": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
          "shared": false, "sharingLevel": 0 }
      ]
    }
  }
}

blocks

query

Fetches blocks by reference rather than by search. Its main job is walking relationships: "give me the subtasks of this task" (parentReferenceBlock) or "give me the notes generated from this meeting" (originReferenceBlock).

Arguments (FindBlocksInput)

_ids[String!]

Fetch a specific set of blocks.

parentReferenceBlockReferenceBlockInput

Children of this block: { id, blockType }.

originReferenceBlockReferenceBlockInput

Blocks generated from this block.

originReferenceTagIdString

Blocks generated for this tag.

blockTypeString

Limit to one kind.

sizeInt

Maximum results.

Returns

A list of Block objects.

Request
query Subtasks {
  blocks(input: {
    parentReferenceBlock: {
      id: "3a5b7c9d-2e4f-4a6b-8c0d-1e3f5a7b9c0d",
      blockType: "task"
    }
  }) {
    _id
    title
    ... on Task { status dateDue }
  }
}
Response
{
  "data": {
    "blocks": [
      { "_id": "5d6e7f8a-9b0c-4d1e-8f2a-3b4c5d6e7f8a",
        "title": "Draft renewal proposal",
        "status": "in_progress",
        "dateDue": 1757601600000 }
    ]
  }
}

searchTasksAndNotes

query

A convenience over searchGraph that returns only tasks and notes, typed as a TaskAndNote union. It takes the same GraphSearchInput. Use it when you are building a to-do-plus-notes view and want the type system to guarantee nothing else sneaks in.

Returns

A list of Task | Note union values - select fields with inline fragments.

Request
query WorkList($input: GraphSearchInput) {
  searchTasksAndNotes(input: $input) {
    ... on Task { _id title status dateDue }
    ... on Note { _id title dateUpdated }
  }
}
Response
{
  "data": {
    "searchTasksAndNotes": [
      { "_id": "3a5b7c9d-2e4f-4a6b-8c0d-1e3f5a7b9c0d",
        "title": "Send pilot agreement",
        "status": "not_started", "dateDue": 1757601600000 },
      { "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
        "title": "QBR prep - Acme", "dateUpdated": 1757083380000 }
    ]
  }
}

blockFabric

query

Returns the web of content around one block: related blocks, the links between them, and the tags involved. Use it to build a "related items" panel. The result is bounded on the server (limited depth, count, and time window), so it stays a sensible size.

Arguments (BlockFabricInput)

_idStringrequired

The block at the center.

dateCreatedNumberFilterInput

Limit related content to a date range.

Returns

BlockFabric: rootId, the related blocks, links (sourceIdtargetId pairs), and the tags involved.

Request
query Related($input: BlockFabricInput!) {
  blockFabric(input: $input) {
    rootId
    blocks { _id blockType title }
    links { sourceId targetId }
    tags { _id alias }
  }
}
Variables
{ "input": { "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b" } }
Response
{
  "data": {
    "blockFabric": {
      "rootId": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "blocks": [
        { "_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9e",
          "blockType": "transcription", "title": "Acme QBR call" }
      ],
      "links": [
        { "sourceId": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
          "targetId": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9e" }
      ],
      "tags": [
        { "_id": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
          "alias": "Acme Robotics" }
      ]
    }
  }
}

blockActivities

query

Returns a block's history: when it was created, renamed, shared, assigned, commented on, or had its status change. Each entry says what happened (activityType), who did it (actorType plus actor), and when. The detail field is a union whose shape depends on the activity type - a rename carries the old and new titles, a status change the old and new statuses, and so on.

Arguments (FindBlockActivitiesInput)

blockIdStringrequired

The block whose history you want.

activityTypes[BlockActivityType]

Only these kinds of events, e.g. [COMMENTED, STATUS_CHANGED].

sizeInt

Maximum entries.

Returns

A list of BlockActivity entries, newest first.

Request
query History($input: FindBlockActivitiesInput!) {
  blockActivities(input: $input) {
    activityType
    actorType
    dateCreated
    detail {
      ... on BlockActivityStatusChangedDetail {
        oldStatus newStatus
      }
    }
  }
}
Variables
{
  "input": {
    "blockId": "3a5b7c9d-2e4f-4a6b-8c0d-1e3f5a7b9c0d",
    "size": 20
  }
}
Response
{
  "data": {
    "blockActivities": [
      {
        "activityType": "STATUS_CHANGED",
        "actorType": "TENANT",
        "dateCreated": 1757083380000,
        "detail": { "oldStatus": "not_started",
                    "newStatus": "in_progress" }
      },
      {
        "activityType": "CREATED",
        "actorType": "TENANT",
        "dateCreated": 1757000000000,
        "detail": {}
      }
    ]
  }
}

upsertOneNote

mutation

Creates a note, or updates one if you pass an existing _id. The usual create is small: a title and the sharingTags that attach the note to the right people and accounts. If you want to control the new note's ID - for example, to navigate to it before the server responds - generate a UUID yourself and pass it as _id. That is what the Noded app does.

Arguments (UpsertNoteInput)

_idString

Omit to create with a server ID; pass a new UUID to create with your ID; pass an existing ID to update.

titleString

The note's title.

sharingTags[SharingTagInput]

Tag links. Remember: shared: true grants access; shared: false only organizes.

documentString

Rich-text body in Noded's editor format. Skip it unless you produce that format.

favoriteBoolean

Star the note.

dateAssociatedFloat

The date the note is "about" (epoch ms) - e.g. the meeting day.

template, templateReferenceBlock, templateInstructions, recommendationIdvarious

Template and feed-integration options; most integrations can ignore them.

Returns

The Note after the write.

Request
mutation CreateNote($input: UpsertNoteInput) {
  upsertOneNote(input: $input) {
    _id
    title
    dateCreated
    sharingTags { tagId shared }
  }
}
Variables
{
  "input": {
    "title": "QBR prep - Acme",
    "sharingTags": [
      { "tagId": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
        "shared": false, "sharingLevel": 0, "sharingApproach": 1 }
    ]
  }
}
Response
{
  "data": {
    "upsertOneNote": {
      "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "title": "QBR prep - Acme",
      "dateCreated": 1757000000000,
      "sharingTags": [
        { "tagId": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
          "shared": false }
      ]
    }
  }
}

upsertOneTask

mutation

Creates or updates a task. On top of the usual block fields, a task has a status, a due date, and an assignee. Assignment works through sharing tags: add the person's tag with assigned: true (and usually shared: true so they can see it).

Key arguments (UpsertTaskInput)

_idID

Omit to create; pass to update.

title, summaryString

What the task is.

statusTaskStatus

not_started, in_progress, or completed.

dateDue, dateRemindMeFloat

Due date and reminder, in epoch ms.

sharingTags[SharingTagInput]

Tag links; use assigned: true on a person's tag to assign the task.

parentReferenceBlockReferenceBlockInput

Make this a subtask of another block.

snoozed, favorite, proposedBoolean

State flags.

Returns

The Task after the write.

Request
mutation CreateTask($input: UpsertTaskInput) {
  upsertOneTask(input: $input) {
    _id
    title
    status
    dateDue
  }
}
Variables
{
  "input": {
    "title": "Send pilot agreement to Jane",
    "status": "not_started",
    "dateDue": 1757601600000,
    "sharingTags": [
      { "tagId": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
        "shared": true, "assigned": true,
        "sharingLevel": 1, "sharingApproach": 1 }
    ]
  }
}
Response
{
  "data": {
    "upsertOneTask": {
      "_id": "3a5b7c9d-2e4f-4a6b-8c0d-1e3f5a7b9c0d",
      "title": "Send pilot agreement to Jane",
      "status": "not_started",
      "dateDue": 1757601600000
    }
  }
}

upsertOneThread

mutation

Creates or updates a thread - a container for a conversation of messages. Most integrations only touch threads to mark them read or unread via readStatus.

Key arguments (UpsertThreadInput)

_idID

Omit to create; pass to update.

titleString

The thread's subject.

readStatusThreadReadStatus

READ or UNREAD.

sharingTags[SharingTagInput]

Tag links, as everywhere.

Returns

The Thread after the write.

Request
mutation MarkRead {
  upsertOneThread(input: {
    _id: "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9e",
    readStatus: READ
  }) {
    _id
    readStatus
    messageCount
  }
}
Response
{
  "data": {
    "upsertOneThread": {
      "_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9e",
      "readStatus": "READ",
      "messageCount": 7
    }
  }
}

upsertOneMessage

mutation

Creates or updates a message - one entry in a conversation, such as a comment on a block. To comment on a note or task, create a message whose parentReferenceBlock points at it.

Key arguments (UpsertMessageInput)

_idID

Omit to create; pass to update.

parentReferenceBlockReferenceBlockInput

The block this message belongs to - e.g. the note being commented on.

bodyHtml, bodyDocumentString

The message content, as HTML or as a Noded editor document.

threadIdString

Attach the message to a thread.

sharingTags[SharingTagInput]

Tag links.

Returns

The Message after the write.

Request
mutation Comment($input: UpsertMessageInput) {
  upsertOneMessage(input: $input) {
    _id
    bodyHtml
    dateCreated
  }
}
Variables
{
  "input": {
    "parentReferenceBlock": {
      "id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "blockType": "note"
    },
    "bodyHtml": "<p>Added the pricing section - please review.</p>"
  }
}
Response
{
  "data": {
    "upsertOneMessage": {
      "_id": "8c9d0e1f-2a3b-4c4d-8e5f-6a7b8c9d0e1f",
      "bodyHtml": "<p>Added the pricing section - please review.</p>",
      "dateCreated": 1757083380000
    }
  }
}

upsertOneDocument

mutation

Creates or updates a document block. Unlike a note, the document body is required here. Use it when your integration produces a full document in Noded's editor format.

Arguments (UpsertDocumentInput)

documentStringrequired

The document body in Noded's editor format.

_idString

Omit to create; pass to update.

metadataType, recommendationId, connectionIdString

Provenance options used by connected tools; usually omitted.

Returns

The Document after the write.

Request
mutation SaveDoc($input: UpsertDocumentInput) {
  upsertOneDocument(input: $input) {
    _id
    title
    dateUpdated
  }
}
Response
{
  "data": {
    "upsertOneDocument": {
      "_id": "0e1f2a3b-4c5d-4e6f-8a7b-8c9d0e1f2a3b",
      "title": "Acme onboarding runbook",
      "dateUpdated": 1757083380000
    }
  }
}

updateOneBlock

mutation

Replaces a block's sharing tags in one call, whatever kind of block it is. This is the bulk tool: it overwrites the whole sharingTags array with what you send. To add or remove a single tag, prefer addSharingTag and deleteSharingTag, which cannot accidentally drop links you did not mean to touch.

Arguments (UpdateBlockInput)

_idString

The block to update.

blockTypeString

Its type, e.g. "note".

sharingTags[SharingTagInput]

The complete new set of tag links.

Returns

The updated Block.

Request
mutation Retag($input: UpdateBlockInput) {
  updateOneBlock(input: $input) {
    _id
    sharingTags { tagId shared pinned }
  }
}
Variables
{
  "input": {
    "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
    "blockType": "note",
    "sharingTags": [
      { "tagId": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
        "shared": false, "pinned": true,
        "sharingLevel": 0, "sharingApproach": 1 }
    ]
  }
}
Response
{
  "data": {
    "updateOneBlock": {
      "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "sharingTags": [
        { "tagId": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
          "shared": false, "pinned": true }
      ]
    }
  }
}

favoriteBlock

mutation

Stars or un-stars a block for the current user.

Arguments (FavoriteBlockInput)

_idStringrequired

The block.

favoriteBooleanrequired

true to star, false to un-star.

Returns

{ block, favorite }.

Request
mutation Star {
  favoriteBlock(input: {
    _id: "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
    favorite: true
  }) {
    favorite
    block { _id title }
  }
}
Response
{
  "data": {
    "favoriteBlock": {
      "favorite": true,
      "block": { "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
                 "title": "QBR prep - Acme" }
    }
  }
}

archiveOneBlock

mutation

Archives a block - or restores it with archive: false. Typed variants exist with identical inputs and outputs: archiveOneNote, archiveOneTask, archiveOneMessage, and archiveOneThread. They all behave the same way; use whichever reads best in your code.

Archiving can be a two-step conversation. If other things depend on the block - an agent watches it, a workflow references it - the first call returns an assessment instead of archiving, describing the impact. Show it to the user, and if they confirm, call again with confirmed: true. If nothing depends on the block, the first call archives it immediately and metadataChange comes back null. Handle both outcomes.

Arguments (ArchiveBlockInput)

_idStringrequired

The block to archive.

blockTypeString

Its type - include it when you know it.

archiveBoolean

true (default) to archive, false to restore.

confirmedBoolean

Pass true on the second call to confirm an assessed change.

Returns

{ _id, archived, metadataChange }. When metadataChange.assessment is present and archived is still false, the change is waiting for confirmation.

Requeststep 1
mutation Archive {
  archiveOneBlock(input: {
    _id: "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
    blockType: "note"
  }) {
    _id
    archived
    metadataChange {
      assessment { impactLevel }
    }
  }
}
Responseneeds confirmation
{
  "data": {
    "archiveOneBlock": {
      "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "archived": false,
      "metadataChange": {
        "assessment": { "impactLevel": "MODERATE" }
      }
    }
  }
}
Requeststep 2 - confirm
mutation ConfirmArchive {
  archiveOneBlock(input: {
    _id: "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
    blockType: "note",
    confirmed: true
  }) {
    _id
    archived
  }
}

addReaction

mutation

Adds an emoji reaction to a block, as the current user. Reactions are grouped by emoji, and each carries the list of tags (people) who reacted with it.

Arguments (AddReactionInput)

_idStringrequired

The block to react to.

blockTypeBlockTyperequired

The block's type.

shortcodesStringrequired

The emoji shortcode, e.g. ":thumbsup:".

Returns

The block's full reaction list after the change.

Request
mutation React {
  addReaction(input: {
    _id: "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
    blockType: note,
    shortcodes: ":thumbsup:"
  }) {
    shortcodes
    tagIds
  }
}
Response
{
  "data": {
    "addReaction": [
      { "shortcodes": ":thumbsup:",
        "tagIds": ["c2d4e6f8-0a1b-4c3d-9e8f-a7b6c5d4e3f2"] }
    ]
  }
}

removeReaction

mutation

Removes the current user's emoji reaction from a block. Same arguments as addReaction, and the same return: the block's remaining reactions.

Request
mutation Unreact {
  removeReaction(input: {
    _id: "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
    blockType: note,
    shortcodes: ":thumbsup:"
  }) {
    shortcodes
    tagIds
  }
}
Response
{ "data": { "removeReaction": [] } }

addBlockFeedback

mutation

Records thumbs-up or thumbs-down feedback on AI-generated content, with an optional comment. Feedback helps Noded improve what it generates for this user.

Arguments (AddBlockFeedbackInput)

referenceBlockReferenceBlockInputrequired

The block the feedback is about: { id, blockType }.

helpfulBooleanrequired

true for thumbs-up, false for thumbs-down.

contentString

An optional written comment.

Returns

The stored BlockFeedback.

Request
mutation Feedback($input: AddBlockFeedbackInput!) {
  addBlockFeedback(input: $input) {
    _id
    helpful
  }
}
Variables
{
  "input": {
    "referenceBlock": {
      "id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "blockType": "note"
    },
    "helpful": true,
    "content": "Good summary of the call."
  }
}
Response
{
  "data": {
    "addBlockFeedback": {
      "_id": "4d5e6f7a-8b9c-4d0e-8f1a-2b3c4d5e6f7a",
      "helpful": true
    }
  }
}

Sharing and linking

These mutations manage the links between tags and content - which is also how access is granted - and the links between the graph and outside systems.

addSharingTag

mutation

Adds one tag link to a block. This is the mutation behind both "tag this note to Acme" and "share this note with Jane" - the difference is the shared flag, as explained in the sharing concept.

You can even share with someone who is not in the graph yet: pass an email instead of a tagId, and Noded creates the person tag and sends an invite.

shared: true grants real access. The person behind the tag will be able to open this block at the level you set. Treat it like any permissions change.

Arguments (AddSharingTagInput)

referenceBlockReferenceBlockInputrequired

The block to link: { id, blockType }.

sharingTag.tagIdString

The tag to link. Either this or email.

sharingTag.emailString

Invite by email when there is no tag yet.

sharingTag.sharedBooleanrequired

false = organize only; true = grant access.

sharingTag.sharingLevelIntrequired

0 read-only, 1 editor, 2 owner.

sharingTag.sharingApproachIntrequired

Use 1 (explicit) for deliberate links from your app.

sharingTag.pinned, sharingTag.assignedBoolean

Pin the block to the tag's page, or assign a task to the person.

Returns

{ block } - the block with its updated sharingTags.

Request
mutation Share($input: AddSharingTagInput) {
  addSharingTag(input: $input) {
    block {
      _id
      sharingTags { tagId shared sharingLevel }
    }
  }
}
Variables
{
  "input": {
    "referenceBlock": {
      "id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "blockType": "note"
    },
    "sharingTag": {
      "tagId": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
      "shared": true, "sharingLevel": 1, "sharingApproach": 1
    }
  }
}
Response
{
  "data": {
    "addSharingTag": {
      "block": {
        "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
        "sharingTags": [
          { "tagId": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
            "shared": false, "sharingLevel": 0 },
          { "tagId": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
            "shared": true, "sharingLevel": 1 }
        ]
      }
    }
  }
}

deleteSharingTag

mutation

Removes one tag link from a block. If the link had shared: true, this also revokes that tag's access.

Arguments (DeleteSharingTagInput)

referenceBlockReferenceBlockInputrequired

The block: { id, blockType }.

tagIdStringrequired

The tag whose link to remove.

Returns

{ block } with the updated sharingTags.

Request
mutation Unshare($input: DeleteSharingTagInput!) {
  deleteSharingTag(input: $input) {
    block { _id sharingTags { tagId } }
  }
}
Variables
{
  "input": {
    "referenceBlock": {
      "id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "blockType": "note"
    },
    "tagId": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d"
  }
}
Response
{
  "data": {
    "deleteSharingTag": {
      "block": {
        "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
        "sharingTags": [
          { "tagId": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c" }
        ]
      }
    }
  }
}

addBusinessObject

mutation

Links a block to an object in an outside system - for example, attaching a note to a Salesforce opportunity. The link shows up in both places: on the block's businessObjects and in Noded's UI.

Arguments (AddBusinessObjectInput)

referenceBlockReferenceBlockInputrequired

The block to link from.

businessObjectBusinessObjectLinkInputrequired

The external object: { url, externalId, provider, metadataType } - a URL alone is often enough.

Returns

{ block } with its updated businessObjects.

Request
mutation LinkCrm($input: AddBusinessObjectInput!) {
  addBusinessObject(input: $input) {
    block {
      _id
      businessObjects { provider url }
    }
  }
}
Variables
{
  "input": {
    "referenceBlock": {
      "id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "blockType": "note"
    },
    "businessObject": {
      "provider": "salesforce",
      "url": "https://acme.my.salesforce.com/0068c00000abcde"
    }
  }
}
Response
{
  "data": {
    "addBusinessObject": {
      "block": {
        "_id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
        "businessObjects": [
          { "provider": "salesforce",
            "url": "https://acme.my.salesforce.com/0068c00000abcde" }
        ]
      }
    }
  }
}

removeBusinessObject

mutation

Removes a business-object link from a block. Pass the block reference and the businessObjectId (the id of the link, from the block's businessObjects list).

Returns

{ block } with its updated businessObjects.

Request
mutation UnlinkCrm($input: RemoveBusinessObjectInput!) {
  removeBusinessObject(input: $input) {
    block { _id businessObjects { id } }
  }
}
Variables
{
  "input": {
    "referenceBlock": {
      "id": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
      "blockType": "note"
    },
    "businessObjectId": "bo-3f4a5b6c"
  }
}

Folders and navigation

Folders group tags and saved searches; the navigation tree is the user's ordered sidebar. One design rule makes navigation easy: every navigation mutation returns the complete new tree, so you re-render from the response instead of patching your local copy.

folders

query

Lists the folders the user can see - their own and the ones shared with them. A folder holds tagIds and searchIds, carries its own sharingTags, and reports your permission through access.

Arguments (FindFoldersInput)

archivedBoolean

Pass false to hide archived folders.

sizeInt

Maximum results.

Returns

A list of Folder objects.

Request
query Folders {
  folders(input: { archived: false }) {
    _id
    label
    icon
    tagIds
    access
  }
}
Response
{
  "data": {
    "folders": [
      {
        "_id": "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b",
        "label": "Key accounts",
        "icon": "star",
        "tagIds": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"],
        "access": "RW"
      }
    ]
  }
}

folder

query

Fetches one folder by ID, including its sharing setup. Use it before editing a folder so you can rewrite sharingTags from the current state.

Arguments (FindFolderInput)

_idStringrequired

The folder's ID.

Returns

One Folder.

Request
query GetFolder {
  folder(input: { _id: "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b" }) {
    _id
    label
    description
    tagIds
    searchIds
    sharingTags { tagId shared sharingLevel }
  }
}
Response
{
  "data": {
    "folder": {
      "_id": "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b",
      "label": "Key accounts",
      "description": "Accounts we review weekly.",
      "tagIds": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"],
      "searchIds": [],
      "sharingTags": [
        { "tagId": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
          "shared": true, "sharingLevel": 1 }
      ]
    }
  }
}

upsertOneFolder

mutation

Creates a folder, or updates one if you pass an _id. This is also how folder sharing changes: there is no add/remove pair for folders, so read the folder, adjust its sharingTags array, and write the whole array back.

Arguments (UpsertFolderInput)

_idString

Omit to create; pass to update.

labelString

The folder's name.

icon, descriptionString

Display details.

tagIds, searchIds[String]

What the folder contains.

sharingTags[SharingTagInput]

The complete sharing set - this replaces what was there.

Returns

The Folder after the write.

Request
mutation SaveFolder($input: UpsertFolderInput) {
  upsertOneFolder(input: $input) {
    _id
    label
    tagIds
  }
}
Variables
{
  "input": {
    "_id": "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b",
    "label": "Key accounts",
    "tagIds": [
      "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
      "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d"
    ]
  }
}
Response
{
  "data": {
    "upsertOneFolder": {
      "_id": "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b",
      "label": "Key accounts",
      "tagIds": [
        "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
        "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d"
      ]
    }
  }
}

archiveOneFolder

mutation

Archives a folder (or restores it with archive: false). Like archiveOneBlock, this can use the two-step confirmation: if agents or workflows depend on the folder, the first call returns an assessment, and you confirm with confirmed: true.

Arguments (ArchiveFolderInput)

_idStringrequired

The folder.

archiveBoolean

true (default) to archive, false to restore.

confirmedBoolean

Confirm an assessed change.

Returns

{ _id, archived, metadataChange }.

Request
mutation ArchiveFolder {
  archiveOneFolder(input: {
    _id: "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b"
  }) {
    _id
    archived
  }
}
Response
{
  "data": {
    "archiveOneFolder": {
      "_id": "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b",
      "archived": true
    }
  }
}

leaveFolderSharing

mutation

Removes the current user from a folder that someone else shared with them. The folder stays intact for everyone else; it just stops appearing for this user.

Arguments (LeaveFolderInput)

_idStringrequired

The folder to leave.

Returns

The Folder after the change.

Request
mutation Leave {
  leaveFolderSharing(input: {
    _id: "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b"
  }) {
    _id
    label
  }
}

navigationTree

query

Returns the user's sidebar: an ordered list of entries, where each entry is either a tag or a folder of tags. The tree is exactly two levels deep - folders cannot nest. It takes no arguments.

Because NavigationEntry is a union, you select each side with an inline fragment, as shown.

Returns

NavigationTree: an _id and the ordered entries.

Request
query Sidebar {
  navigationTree {
    entries {
      ... on NavigationTagEntry {
        order
        tag { _id alias }
      }
      ... on NavigationFolderEntry {
        order
        folder { _id label }
        children { order tag { _id alias } }
      }
    }
  }
}
Response
{
  "data": {
    "navigationTree": {
      "entries": [
        {
          "order": 0,
          "tag": { "_id": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
                   "alias": "Jane Rivera" }
        },
        {
          "order": 1,
          "folder": { "_id": "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b",
                      "label": "Key accounts" },
          "children": [
            { "order": 0,
              "tag": { "_id": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
                       "alias": "Acme Robotics" } }
          ]
        }
      ]
    }
  }
}

setNavigation

mutation

Replaces the entire sidebar in one call - the bulk reorder. You pass the full list of items in their new order; each item names a tag or folder by _id and blockType, and a folder item lists its childTagIds in order.

Arguments ([NavigationItemRefInput!]!)

_idStringrequired

The tag or folder ID.

blockTypeBlockTyperequired

tag or folder.

childTagIds[String!]

For a folder: its tags, in order.

Returns

{ navigationTree } - the complete new tree.

Request
mutation Reorder($input: [NavigationItemRefInput!]!) {
  setNavigation(input: $input) {
    navigationTree {
      entries {
        ... on NavigationTagEntry { order tag { _id } }
        ... on NavigationFolderEntry { order folder { _id } }
      }
    }
  }
}
Variables
{
  "input": [
    { "_id": "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b",
      "blockType": "folder",
      "childTagIds": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"] },
    { "_id": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
      "blockType": "tag" }
  ]
}

addTagToNavigation

mutation

Pins a tag to the top level of the sidebar.

Arguments (AddTagToNavigationInput)

tagIdStringrequired

The tag to add.

Returns

{ navigationTree } - the complete new tree.

Request
mutation Pin {
  addTagToNavigation(input: {
    tagId: "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"
  }) {
    navigationTree { _id }
  }
}

removeTagFromNavigation

mutation

Removes a tag from the sidebar. The tag itself is untouched - only the navigation entry goes away.

Arguments (RemoveTagFromNavigationInput)

tagIdStringrequired

The tag to remove.

Returns

{ navigationTree }.

Request
mutation Unpin {
  removeTagFromNavigation(input: {
    tagId: "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"
  }) {
    navigationTree { _id }
  }
}

createNavigationFolder

mutation

Creates a folder and places it in the sidebar in one step. You can seed it with tags right away.

Arguments (NavigationNewFolderInput)

labelStringrequired

The folder's name.

icon, descriptionString

Display details.

tagIds[String]

Tags to put inside.

sharingTags[SharingTagInput]

Share the folder as you create it.

Returns

{ folder, navigationTree } - the new folder and the complete new tree.

Request
mutation NewFolder($input: NavigationNewFolderInput!) {
  createNavigationFolder(input: $input) {
    folder { _id label }
    navigationTree { _id }
  }
}
Variables
{
  "input": {
    "label": "Renewals Q1",
    "tagIds": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"]
  }
}
Response
{
  "data": {
    "createNavigationFolder": {
      "folder": { "_id": "a9b8c7d6-e5f4-4a3b-8c2d-1e0f9a8b7c6e",
                  "label": "Renewals Q1" },
      "navigationTree": { "_id": "nav-c2d4e6f8" }
    }
  }
}

addTagToFolder

mutation

Puts a tag inside a sidebar folder, optionally at a specific position.

Arguments (AddTagToFolderInput)

folderIdStringrequired

The folder.

tagIdStringrequired

The tag to add.

positionInt

0-based position; omit to append.

Returns

{ navigationTree }.

Request
mutation FileTag {
  addTagToFolder(input: {
    folderId: "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b",
    tagId: "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
    position: 0
  }) {
    navigationTree { _id }
  }
}

removeTagFromFolder

mutation

Takes a tag out of a sidebar folder. The tag and the folder both continue to exist.

Arguments (RemoveTagFromFolderInput)

folderIdStringrequired

The folder.

tagIdStringrequired

The tag to remove.

Returns

{ navigationTree }.

Request
mutation UnfileTag {
  removeTagFromFolder(input: {
    folderId: "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b",
    tagId: "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d"
  }) {
    navigationTree { _id }
  }
}

moveNavigationItem

mutation

Moves a tag around the sidebar - between folders, out of a folder to the top level, or to a new position. This is the drag-and-drop mutation.

Arguments (MoveNavigationItemInput)

tagIdStringrequired

The tag being moved.

fromFolderIdString

Where it currently lives; omit if it is at the top level.

toFolderIdString

Where it should go; omit to move it to the top level.

positionInt

0-based position at the destination.

Returns

{ navigationTree }.

Request
mutation Move {
  moveNavigationItem(input: {
    tagId: "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
    fromFolderId: "f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b",
    position: 1
  }) {
    navigationTree { _id }
  }
}

Saved searches

A saved search is a stored, shareable set of filters - the same vocabulary you pass to searchGraph, kept under a name. Folders can hold saved searches next to tags.

searches

query

Lists the user's saved searches. Each one stores a label plus its filter settings.

Arguments (FindSearchesInput)

_ids[String]

Fetch specific saved searches.

archivedBoolean

Defaults to false.

sizeInt

Maximum results.

Returns

A list of Search objects.

Request
query SavedSearches {
  searches {
    _id
    label
    blockType
    tagIds
    status
  }
}
Response
{
  "data": {
    "searches": [
      {
        "_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
        "label": "Open Acme tasks",
        "blockType": "task",
        "tagIds": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"],
        "status": "not_started"
      }
    ]
  }
}

upsertOneSearch

mutation

Creates or updates a saved search. Give it a label and whichever filters you want stored - block type, tags, a text query, a status, date-due bounds, a sort. Add sharingTags to share the saved search with teammates.

Key arguments (UpsertSearchInput)

_idString

Omit to create; pass to update.

labelString

The saved search's name.

search, blockType, tagIds, emails, status, favorite, snoozedvarious

The stored filters.

dateDueFrom, dateDueToFloat

Due-date bounds, epoch ms.

orderByDateUpdated, orderByDateCreated, orderByDateDue, orderByAliasString

Stored sort: "asc" or "desc".

sharingTags[SharingTagInput]

Share the saved search.

Returns

The Search after the write.

Request
mutation SaveSearch($input: UpsertSearchInput) {
  upsertOneSearch(input: $input) {
    _id
    label
  }
}
Variables
{
  "input": {
    "label": "Open Acme tasks",
    "blockType": "task",
    "tagIds": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"],
    "status": "not_started",
    "orderByDateDue": "asc"
  }
}
Response
{
  "data": {
    "upsertOneSearch": {
      "_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
      "label": "Open Acme tasks"
    }
  }
}

archiveOneSearch

mutation

Archives a saved search, or restores it with archive: false.

Arguments (ArchiveSearchInput)

_idStringrequired

The saved search.

archiveBoolean

true (default) or false.

Returns

{ _id, archived }.

Request
mutation DropSearch {
  archiveOneSearch(input: {
    _id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
  }) {
    _id
    archived
  }
}

Tables, records, and signals

Structured data in the graph. Read tables and records for display; write through record types, which declare what your integration is allowed to do; use signals for the single values that matter on a tag.

tables

query

Lists the structured tables in the graph. Ask for metadata to learn each table's columns - you will need the field names to read record values and to write.

Arguments (FindTablesInput)

_ids[String]

Specific tables.

provider, applicationString

Only tables synced from this system.

tagType, tagSubTypeString

Only tables attached to this kind of tag.

tasky, generatedBoolean

Task-like tables, or tables Noded generated.

sizeInt

Maximum results.

Returns

A list of Table objects.

Request
query Tables {
  tables(input: { provider: "salesforce" }) {
    _id
    metadata {
      label
      metadataType
      fields { name label fieldType }
    }
  }
}
Response
{
  "data": {
    "tables": [
      {
        "_id": "0d1e2f3a-4b5c-4d6e-8f9a-b0c1d2e3f4a5",
        "metadata": {
          "label": "Accounts",
          "metadataType": "Account",
          "fields": [
            { "name": "account_name", "label": "Account name",
              "fieldType": "text" },
            { "name": "renewal_date", "label": "Renewal date",
              "fieldType": "date" },
            { "name": "health_score", "label": "Health score",
              "fieldType": "number" }
          ]
        }
      }
    ]
  }
}

record

query

Fetches one record. A quirk worth knowing: the return type is Table, not Record - you get the record's table with its metadata, and the record itself sits inside records. That way the column definitions always travel with the data.

Arguments (FindRecordInput)

_idString

The record's ID.

uriString

Alternatively, look the record up by its external URI.

recommendationIdString

Scope to a feed recommendation, to preview the values it proposes.

Returns

A Table containing the one record and its metadata.

Request
query GetRecord {
  record(input: { _id: "d8e9f0a1-2b3c-4d5e-8f9a-0b1c2d3e4f5a" }) {
    metadata { fields { name label } }
    records {
      _id
      values { name value reasoning }
    }
  }
}
Response
{
  "data": {
    "record": {
      "metadata": {
        "fields": [
          { "name": "account_name", "label": "Account name" },
          { "name": "health_score", "label": "Health score" }
        ]
      },
      "records": [
        {
          "_id": "d8e9f0a1-2b3c-4d5e-8f9a-0b1c2d3e4f5a",
          "values": [
            { "name": "account_name", "value": "Acme Robotics",
              "reasoning": null },
            { "name": "health_score", "value": "72",
              "reasoning": "Usage steady; two escalations open." }
          ]
        }
      ]
    }
  }
}

searchGraphRecords

query

Searches records with the same GraphSearchInput filters as searchGraph - scope by tag to get "the records on this account." Results come back grouped by table, so metadata rides along.

Returns

A list of Table objects, each holding the matching records.

Request
query AccountRecords($input: GraphSearchInput) {
  searchGraphRecords(input: $input) {
    _id
    metadata { label }
    records { _id values { name value } }
  }
}
Variables
{
  "input": {
    "tags": [{ "_ids": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"] }]
  }
}

recordTypes

query

Lists the record types - the write contracts over tables. Before writing structured data, find the record type for your table and check its operations: only the operations marked true will succeed.

Arguments (FindRecordTypesInput)

tableIdString

Only record types over this table.

archivedBoolean

Include archived record types.

Returns

A list of RecordType objects.

Request
query WriteContracts {
  recordTypes(input: {}) {
    _id
    label
    tableId
    enabled
    operations { create read update }
  }
}
Response
{
  "data": {
    "recordTypes": [
      {
        "_id": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
        "label": "Meeting summary",
        "tableId": "0d1e2f3a-4b5c-4d6e-8f9a-b0c1d2e3f4a5",
        "enabled": true,
        "operations": { "create": true, "read": true,
                        "update": false }
      }
    ]
  }
}

recordType

query

Fetches one record type in full, including its fields - which fields a write accepts, which are required, and any per-field options or prompts.

Arguments (FindRecordTypeInput)

_idStringrequired

The record type's ID.

Returns

One RecordType.

Request
query Contract {
  recordType(input: { _id: "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f" }) {
    label
    operations { create read update }
    fields { name label required visible }
  }
}
Response
{
  "data": {
    "recordType": {
      "label": "Meeting summary",
      "operations": { "create": true, "read": true,
                      "update": false },
      "fields": [
        { "name": "subject", "label": "Subject",
          "required": true, "visible": true },
        { "name": "next_steps", "label": "Next steps",
          "required": false, "visible": true }
      ]
    }
  }
}

executeRecordTypeCreate

mutation

Creates a record through a record type. Pass the field values as name-value pairs, using the field names from the record type. If the table is synced from an outside system, the write flows through to it. Requires operations.create to be true.

Arguments (ExecuteRecordTypeCreateInput)

recordTypeIdStringrequired

The record type to write through.

values[FieldValueInput!]required

The new record's fields: [{ name, value }].

contextTagIdString

The tag this record is about - links it to the right account or person.

Returns

{ record } - the created record.

Request
mutation CreateRecord($input: ExecuteRecordTypeCreateInput!) {
  executeRecordTypeCreate(input: $input) {
    record { _id values { name value } }
  }
}
Variables
{
  "input": {
    "recordTypeId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
    "contextTagId": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
    "values": [
      { "name": "subject", "value": "QBR follow-up" },
      { "name": "next_steps", "value": "Send pilot agreement." }
    ]
  }
}
Response
{
  "data": {
    "executeRecordTypeCreate": {
      "record": {
        "_id": "e0f1a2b3-c4d5-4e6f-8a7b-8c9d0e1f2a3c",
        "values": [
          { "name": "subject", "value": "QBR follow-up" },
          { "name": "next_steps", "value": "Send pilot agreement." }
        ]
      }
    }
  }
}

executeRecordTypeRead

mutation

Searches records through a record type using field-level criteria - including records that live in the connected system rather than in Noded. Each criterion names a field, an operator, and a value. Requires operations.read.

Arguments (ExecuteRecordTypeReadInput)

recordTypeIdStringrequired

The record type to read through.

searchCriteria[SearchCriterionInput!]required

{ fieldName, operator, value } triples. Operators: equals, contains, starts_with, gte, lte, between.

limitInt

Maximum results.

Returns

{ records, hasMore } - each record with a title, provenance, and its values.

Request
mutation FindRecords($input: ExecuteRecordTypeReadInput!) {
  executeRecordTypeRead(input: $input) {
    hasMore
    records { title externalId values { name value } }
  }
}
Variables
{
  "input": {
    "recordTypeId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
    "searchCriteria": [
      { "fieldName": "subject", "operator": "contains",
        "value": "QBR" }
    ],
    "limit": 10
  }
}
Response
{
  "data": {
    "executeRecordTypeRead": {
      "hasMore": false,
      "records": [
        {
          "title": "QBR follow-up",
          "externalId": "a0Bqwerty123",
          "values": [
            { "name": "subject", "value": "QBR follow-up" }
          ]
        }
      ]
    }
  }
}

executeRecordTypeUpdate

mutation

Updates one record through a record type. Send only the fields you are changing. Requires operations.update.

Arguments (ExecuteRecordTypeUpdateInput)

recordTypeIdStringrequired

The record type.

recordIdStringrequired

The record to update.

values[FieldValueInput!]required

The fields to change.

contextTagIdString

The tag this change relates to.

Returns

{ record } - the updated record.

Request
mutation UpdateRecord($input: ExecuteRecordTypeUpdateInput!) {
  executeRecordTypeUpdate(input: $input) {
    record { _id values { name value } }
  }
}
Variables
{
  "input": {
    "recordTypeId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
    "recordId": "e0f1a2b3-c4d5-4e6f-8a7b-8c9d0e1f2a3c",
    "values": [
      { "name": "next_steps", "value": "Agreement signed." }
    ]
  }
}

upsertManyRecords

mutation

Writes several records of one table at once. This is the lower-level bulk write the Noded app uses for its record grid; each entry names an existing record _id and the values to set. For creating new rows, prefer executeRecordTypeCreate.

Arguments (UpsertRecordsInput)

_idStringrequired

The table's ID.

records[RecordInput]

Each { _id, values } - the record and the fields to set.

Returns

The Table with its updated records.

Request
mutation BulkWrite($input: UpsertRecordsInput) {
  upsertManyRecords(input: $input) {
    _id
    records { _id values { name value } }
  }
}
Variables
{
  "input": {
    "_id": "0d1e2f3a-4b5c-4d6e-8f9a-b0c1d2e3f4a5",
    "records": [
      { "_id": "d8e9f0a1-2b3c-4d5e-8f9a-0b1c2d3e4f5a",
        "values": [{ "name": "health_score", "value": "78" }] }
    ]
  }
}

availableSignals

query

Lists the signals that exist for a kind of tag - which fields, from which tables and providers, can appear on an account or person page. Use it to build a signal picker. To read a specific tag's signal values, select the signals field on the tag itself.

Arguments (AvailableSignalsInput)

tagType, tagSubTypeString

The kind of tag, e.g. topic / organization.

tableIdID

Limit to one table.

Returns

{ groups } - one group per table, each listing its signals.

Request
query Signals {
  availableSignals(input: {
    tagType: topic, tagSubType: "organization"
  }) {
    groups {
      table { _id metadata { label } }
      signals { field { name label } }
    }
  }
}
Response
{
  "data": {
    "availableSignals": {
      "groups": [
        {
          "table": { "_id": "0d1e2f3a-4b5c-4d6e-8f9a-b0c1d2e3f4a5",
                     "metadata": { "label": "Accounts" } },
          "signals": [
            { "field": { "name": "renewal_date",
                         "label": "Renewal date" } },
            { "field": { "name": "health_score",
                         "label": "Health score" } }
          ]
        }
      ]
    }
  }
}

setSignalValue

mutation

Overrides a signal's value for one tag. The override sits on top of whatever the sync or the AI computed, and it stays until you remove it, until it expires, or until a trigger you set clears it. A reason is required so teammates can see why the number was pinned.

Arguments (SetSignalValueInput)

tagIdIDrequired

The tag whose signal to override.

tableIdIDrequired

The table the field belongs to.

fieldNameStringrequired

The field to override.

reasonStringrequired

Why - shown alongside the value.

valueString

The pinned value.

dateOverrideExpiresFloat

When the override should lapse, epoch ms.

overrideTriggerBlockTypes[String!]

Clear the override when this kind of new content arrives, e.g. ["transcription"].

Returns

{ signal } - the signal with its new value.

Request
mutation Pin($input: SetSignalValueInput!) {
  setSignalValue(input: $input) {
    signal {
      field { name }
      value { value reasoning source }
    }
  }
}
Variables
{
  "input": {
    "tagId": "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
    "tableId": "0d1e2f3a-4b5c-4d6e-8f9a-b0c1d2e3f4a5",
    "fieldName": "health_score",
    "value": "40",
    "reason": "Exec sponsor left; treating as at-risk."
  }
}
Response
{
  "data": {
    "setSignalValue": {
      "signal": {
        "field": { "name": "health_score" },
        "value": { "value": "40",
                   "reasoning": "Exec sponsor left; treating as at-risk.",
                   "source": "user" }
      }
    }
  }
}

removeSignalOverride

mutation

Removes a signal override, letting the synced or computed value show again.

Arguments (RemoveSignalOverrideInput)

tagId, tableId, fieldNamevariousrequired

The same coordinates you used to set the override.

Returns

{ signal } with the restored value.

Request
mutation Unpin {
  removeSignalOverride(input: {
    tagId: "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c",
    tableId: "0d1e2f3a-4b5c-4d6e-8f9a-b0c1d2e3f4a5",
    fieldName: "health_score"
  }) {
    signal { value { value source } }
  }
}

upsertOneTable

mutation

Changes a table's schema by replacing its field list. Removing or reshaping a field can break agents, signals, and record types that depend on it - so this mutation uses the two-step confirmation: the first call may return an assessment in metadataChange, and you repeat the call with confirmed: true to proceed.

Arguments (UpsertOneTableInput)

_idStringrequired

The table.

fields[FieldInput!]required

The complete new field list.

confirmedBoolean

Confirm an assessed change.

Returns

{ table, metadataChange }.

Request
mutation EditSchema($input: UpsertOneTableInput!) {
  upsertOneTable(input: $input) {
    table { _id metadata { fields { name } } }
    metadataChange { assessment { impactLevel } }
  }
}

archiveOneTable

mutation

Archives a whole table. Because this affects every record, signal, and record type built on it, expect the two-step confirmation: assess first, then repeat with confirmed: true.

Arguments (ArchiveOneTableInput)

_idStringrequired

The table to archive.

confirmedBoolean

Confirm an assessed change.

Returns

{ _id, archived, metadataChange }.

Request
mutation DropTable {
  archiveOneTable(input: {
    _id: "0d1e2f3a-4b5c-4d6e-8f9a-b0c1d2e3f4a5"
  }) {
    _id
    archived
    metadataChange { assessment { impactLevel } }
  }
}

Recommendations: the feed

The feed is Noded's inbox of things it did or noticed for the user. Read it with one query; act on items with the mutations below.

searchRecommendations

query

Returns feed items. For most uses, select the display fields and render them as-is - they are written to be shown. Scope by tagIds to build a per-account activity panel.

Note that this operation uses its own input type, SearchInput, which is older and simpler than GraphSearchInput - sorting, for example, is orderByDateCreated: "desc" rather than a sort array.

Key arguments (SearchInput)

tagIds[String]

Feed items about these tags.

_ids[String]

Specific items.

recommendationTypeString

One kind only: CONNECT, SHARE, UPSERT, NOTIFICATION, INGESTION, SYNC_PAUSED, METADATA_CHANGE.

referenceBlockReferenceBlockInput

Items that point at this block.

dateCreatedNumberFilterInput

Date range, epoch ms.

orderByDateCreated, orderByDateUpdated, orderByDateDueString

"asc" or "desc".

size, pageInt

Paging; the app uses size: 100.

Returns

A list of Recommendation objects.

Request
query Feed($input: SearchInput) {
  searchRecommendations(input: $input) {
    _id
    recommendationType
    displayLabel
    displayTitle
    displayDescription
    navigationTargetEntityType
    navigationTargetId
    dateCreated
  }
}
Variables
{
  "input": {
    "tagIds": ["b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"],
    "orderByDateCreated": "desc",
    "size": 20
  }
}
Response
{
  "data": {
    "searchRecommendations": [
      {
        "_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
        "recommendationType": "UPSERT",
        "displayLabel": "Drafted note",
        "displayTitle": "Acme QBR call summary",
        "displayDescription": "Noded drafted a summary of yesterday's call.",
        "navigationTargetEntityType": "block",
        "navigationTargetId": "7e8f9a0b-1c2d-4e3f-8a9b-0c1d2e3f4a5b",
        "dateCreated": 1757000000000
      }
    ]
  }
}

updateOneRecommendation

mutation

Updates a feed item's user-facing state: thumbs feedback, a comment, its read status, or whether it is completed.

Arguments (UpdateRecommendationInput)

_idStringrequired

The feed item.

thumbsString

"up" or "down".

commentString

Written feedback.

readStatusThreadReadStatus

READ or UNREAD.

completedBoolean

Mark the item done.

Returns

The updated Recommendation.

Request
mutation Thumbs {
  updateOneRecommendation(input: {
    _id: "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
    thumbs: "up"
  }) {
    _id
    thumbs
  }
}
Response
{
  "data": {
    "updateOneRecommendation": {
      "_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "thumbs": "up"
    }
  }
}

archiveOneRecommendation

mutation

Archives one feed item - the "clear from my feed" action. Restore with archive: false.

Arguments (ArchiveRecommendationInput)

_idStringrequired

The feed item.

archiveBoolean

true (default) or false.

Returns

{ _id, archived }.

Request
mutation Clear {
  archiveOneRecommendation(input: {
    _id: "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"
  }) {
    _id
    archived
  }
}

archiveAllRecommendations

mutation

Clears the user's entire feed in one call. It takes no arguments, so ask the user before you call it.

Returns

A list of { _id, archived } results, one per cleared item.

Request
mutation ClearAll {
  archiveAllRecommendations {
    _id
    archived
  }
}

dismissOneRecommendation

mutation

Dismisses a feed item - a stronger signal than archiving. Dismissal tells Noded the suggestion was not wanted, which teaches it to make fewer suggestions like it.

Arguments (DismissRecommendationInput)

_idStringrequired

The feed item to dismiss.

Returns

{ recommendation } - the dismissed item.

Request
mutation Dismiss {
  dismissOneRecommendation(input: {
    _id: "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"
  }) {
    recommendation { _id dismissed }
  }
}
Response
{
  "data": {
    "dismissOneRecommendation": {
      "recommendation": {
        "_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
        "dismissed": true
      }
    }
  }
}

Memory

What the graph has learned about people and accounts, as topic-and-value entries. Read it to give your app context; write it to teach the graph what your app knows.

userMemories

query

Returns all of the user's memories, across every tag and context. Each memory groups entries under a context, and carries the tagId it is about (if any). It takes no arguments.

Returns

A list of UserMemory objects.

Request
query AllMemory {
  userMemories {
    _id
    context
    tagAlias
    entries { topic value confidence }
  }
}
Response
{
  "data": {
    "userMemories": [
      {
        "_id": "8f9a0b1c-2d3e-4f4a-9b5c-6d7e8f9a0b1c",
        "context": "relationship",
        "tagAlias": "Jane Rivera",
        "entries": [
          { "topic": "communication style",
            "value": "Prefers short emails; responds after 5pm.",
            "confidence": "high" }
        ]
      }
    ]
  }
}

userMemoriesForTag

query

Returns the memory for one person or account. This is the call to make when you want to show "what Noded knows" on a profile panel, or to give an AI feature context about who it is writing to.

Arguments (FindUserMemoriesForTagInput)

tagIdStringrequired

The person or account tag.

Returns

One UserMemory, or null if nothing has been learned yet.

Request
query TagMemory {
  userMemoriesForTag(input: {
    tagId: "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d"
  }) {
    _id
    entries { topic value confidence source dateUpdated }
  }
}
Response
{
  "data": {
    "userMemoriesForTag": {
      "_id": "8f9a0b1c-2d3e-4f4a-9b5c-6d7e8f9a0b1c",
      "entries": [
        { "topic": "communication style",
          "value": "Prefers short emails; responds after 5pm.",
          "confidence": "high", "source": "email",
          "dateUpdated": 1756900000000 },
        { "topic": "role",
          "value": "VP Operations; owns the renewal decision.",
          "confidence": "medium", "source": "transcription",
          "dateUpdated": 1756500000000 }
      ]
    }
  }
}

addUserMemoryEntry

mutation

Adds one entry to the user's memory - your integration teaching the graph something it learned. Pick a clear, reusable topic; a later entry with the same topic replaces the value.

Arguments (AddUserMemoryEntryInput)

contextStringrequired

Which memory to add to, e.g. "relationship".

entryMemoryEntryInputrequired

{ topic, value, confidence?, source? }.

tagId, tagAliasString

The tag this memory is about.

Returns

The UserMemory after the write.

Request
mutation Teach($input: AddUserMemoryEntryInput!) {
  addUserMemoryEntry(input: $input) {
    _id
    entries { topic value }
  }
}
Variables
{
  "input": {
    "context": "relationship",
    "tagId": "e7a9c3b1-5f4d-4a2b-8c6d-9e8f7a6b5c4d",
    "entry": {
      "topic": "escalation contact",
      "value": "Prefers Slack over email for urgent issues.",
      "source": "my-app"
    }
  }
}

removeUserMemoryEntry

mutation

Removes one entry from a memory, identified by the memory's ID and the entry's topic.

Arguments (RemoveUserMemoryEntryInput)

memoryIdStringrequired

The memory holding the entry.

entryTopicStringrequired

The topic of the entry to remove.

Returns

true when the entry was removed.

Request
mutation Forget {
  removeUserMemoryEntry(input: {
    memoryId: "8f9a0b1c-2d3e-4f4a-9b5c-6d7e8f9a0b1c",
    entryTopic: "escalation contact"
  })
}
Response
{ "data": { "removeUserMemoryEntry": true } }

deleteUserMemory

mutation

Deletes one whole memory - every entry in it. Note the argument name here is id, not _id.

Arguments (DeleteUserMemoryInput)

idStringrequired

The memory to delete.

Returns

true when deleted.

Request
mutation Delete {
  deleteUserMemory(input: {
    id: "8f9a0b1c-2d3e-4f4a-9b5c-6d7e8f9a0b1c"
  })
}

deleteAllUserMemories

mutation

Deletes everything the graph has learned for this user. This is the "forget me" control. It takes no arguments and cannot be undone - always confirm with the user first.

Returns

true when done.

Request
mutation ForgetEverything {
  deleteAllUserMemories
}
Response
{ "data": { "deleteAllUserMemories": true } }

Agents

Agents are the user's long-horizon workers in Noded: each one watches part of the graph and acts on a goal. The API lets you list them, inspect their runs, manage them, and trigger one on demand.

agents

query

Lists the user's agents. Each agent has a name, an on/off switch (enabled), a live executionState, and a trigger describing when and where it runs. It takes no arguments.

Returns

A list of Agent objects.

Request
query MyAgents {
  agents {
    _id
    name
    description
    enabled
    executionState
    trigger { type frequency }
  }
}
Response
{
  "data": {
    "agents": [
      {
        "_id": "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7c",
        "name": "Renewal watcher",
        "description": "Flags accounts within 90 days of renewal.",
        "enabled": true,
        "executionState": "IDLE",
        "trigger": { "type": "scheduled", "frequency": "daily" }
      }
    ]
  }
}

agent

query

Fetches one agent in full - its trigger, the scope it watches, the notes and tasks it produced, and its memories.

Arguments (FindAgentInput)

_idStringrequired

The agent's ID.

Returns

One Agent.

Request
query GetAgent {
  agent(input: { _id: "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7c" }) {
    name
    enabled
    trigger { type query frequency scope { tags folders } }
    tasks { _id title status }
  }
}

agentExecutions

query

Lists an agent's runs - when each started and finished, its status, a summary, and what it produced. Careful with the naming: the required _id is the agent's ID, not an execution's.

Arguments (FindAgentExecutionsInput)

_idIDrequired

The agent whose runs to list.

tagIdID

Only runs about this tag.

Returns

A list of AgentExecution objects.

Request
query Runs {
  agentExecutions(input: {
    _id: "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7c"
  }) {
    _id
    status
    summary
    dateStarted
    dateCompleted
  }
}
Response
{
  "data": {
    "agentExecutions": [
      {
        "_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
        "status": "completed",
        "summary": "Flagged Acme Robotics: renewal in 87 days.",
        "dateStarted": 1757000000000,
        "dateCompleted": 1757000090000
      }
    ]
  }
}

upsertOneAgent

mutation

Creates or updates an agent. The heart of it is the trigger: what kind of trigger it is, the goal (query), how often it runs, and the scope of the graph it watches - tags, folders, block types, or watched fields.

Key arguments (UpsertAgentInput)

_idID

Omit to create; pass to update.

name, descriptionString

What the agent is.

enabledBoolean

On or off; defaults to on.

triggerAgentTriggerInput

{ type, query, frequency?, scope: { tags, folders, blockTypes, fields } }.

grants[PersonaToolGrantInput!]

What the agent may do, and how autonomously (ask, notify, or silent).

sharingTags, threadInstructions, outputs, templateIdvarious

Sharing and behavior details.

Returns

The Agent after the write.

Request
mutation SaveAgent($input: UpsertAgentInput!) {
  upsertOneAgent(input: $input) {
    _id
    name
    enabled
  }
}
Variables
{
  "input": {
    "name": "Renewal watcher",
    "description": "Flags accounts within 90 days of renewal.",
    "trigger": {
      "type": "scheduled",
      "frequency": "daily",
      "query": "Flag accounts whose renewal is inside 90 days.",
      "scope": {
        "folders": ["f1e2d3c4-b5a6-4978-8a1b-2c3d4e5f6a7b"],
        "tags": [], "blockTypes": [], "fields": []
      }
    }
  }
}

archiveOneAgent

mutation

Archives an agent so it stops running and leaves the user's list. Restore with archive: false.

Arguments (ArchiveAgentInput)

_idIDrequired

The agent.

archiveBoolean

true (default) or false.

Returns

{ _id, archived }.

Request
mutation Retire {
  archiveOneAgent(input: {
    _id: "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7c"
  }) {
    _id
    archived
  }
}

triggerAgent

mutation

Runs an agent right now instead of waiting for its schedule. Pass a tagId to run it for one account or person. The call returns as soon as the run is queued - follow progress with agentExecutions or the agentUpdated subscription.

Arguments (TriggerAgentInput)

_idStringrequired

The agent to run.

tagIdString

Run it for this tag only.

Returns

{ _id, executionState } - usually PENDING.

Request
mutation RunNow {
  triggerAgent(input: {
    _id: "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7c",
    tagId: "b4f6a2c8-1d2e-4f5a-9b8c-7d6e5f4a3b2c"
  }) {
    _id
    executionState
  }
}
Response
{
  "data": {
    "triggerAgent": {
      "_id": "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7c",
      "executionState": "PENDING"
    }
  }
}

Your account

The authenticated user's own profile and settings, plus two utilities: file uploads and URL ingestion.

tenant

query

Returns the authenticated user. Call it with no input to get yourself. The field to remember is tagId - the user's own person tag, which you will use in "assigned to me" and sharing filters everywhere else.

clientSettings is a free-form JSON object your app can use to store small per-user preferences; write it back through updateOneTenant.

Returns

A TenantLight: _id, email, givenName, familyName, picture, tagId, tag, clientSettings, and more.

Request
query Me {
  tenant {
    _id
    email
    givenName
    familyName
    tagId
    clientSettings
  }
}
Response
{
  "data": {
    "tenant": {
      "_id": "6f2a9c1e-8b3d-4e5f-9a7b-1c2d3e4f5a6b",
      "email": "you@yourcompany.com",
      "givenName": "Sam",
      "familyName": "Chen",
      "tagId": "c2d4e6f8-0a1b-4c3d-9e8f-a7b6c5d4e3f2",
      "clientSettings": { "myApp.defaultView": "timeline" }
    }
  }
}

updateOneTenant

mutation

Updates the user's profile or settings. Pass the user's _id plus the fields to change. When writing clientSettings, namespace your keys (for example "myApp.defaultView") so you do not collide with the Noded app's own settings.

Arguments (TenantLightInput)

_idIDrequired

The user's ID (from tenant).

givenName, familyNameString

Profile name.

clientSettingsJSON

Free-form settings object; merged per key.

autoTitleGenerationEnabledBoolean

Whether Noded auto-titles new notes.

Returns

The updated TenantLight.

Request
mutation SavePref($input: TenantLightInput) {
  updateOneTenant(input: $input) {
    _id
    clientSettings
  }
}
Variables
{
  "input": {
    "_id": "6f2a9c1e-8b3d-4e5f-9a7b-1c2d3e4f5a6b",
    "clientSettings": { "myApp.defaultView": "board" }
  }
}

generateUploadUri

mutation

Gets a pre-signed upload target for attaching a file. You receive a uri to POST the file to, the extra form fields that must go with it, and the publicUri where the file will live afterward - which you can then use in a block's content.

Arguments (GenerateUploadUriInput)

_idStringrequired

An ID for the file - generate a UUID.

contentTypeStringrequired

The file's MIME type, e.g. "image/png".

Returns

{ uri, fields, publicUri }.

Request
mutation UploadTarget {
  generateUploadUri(input: {
    _id: "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5e",
    contentType: "image/png"
  }) {
    uri
    fields
    publicUri
  }
}
Response
{
  "data": {
    "generateUploadUri": {
      "uri": "https://uploads.getnoded.ai",
      "fields": { "key": "files/1a2b3c4d...",
                  "policy": "eyJleHBpcmF0aW9uIjo..." },
      "publicUri": "https://files.getnoded.ai/1a2b3c4d..."
    }
  }
}

ingestLinkUrl

mutation

Hands a URL to Noded to read and add to the graph as a website block. Ingestion runs in the background; the response gives you the sync execution so you can check on it.

Arguments (IngestLinkUrlInput)

uriStringrequired

The URL to ingest.

originReferenceBlockReferenceBlockInput

The block where the link was found, if any.

Returns

{ syncExecution } - its status moves from queued to completed.

Request
mutation Ingest {
  ingestLinkUrl(input: {
    uri: "https://acmerobotics.com/pricing"
  }) {
    syncExecution { _id status }
  }
}
Response
{
  "data": {
    "ingestLinkUrl": {
      "syncExecution": {
        "_id": "c3d4e5f6-a7b8-4c9d-8e0f-1a2b3c4d5e6f",
        "status": "queued"
      }
    }
  }
}

Realtime: subscriptions

The graph changes constantly - syncs bring in email and meetings, teammates edit notes, agents produce work. Subscriptions let your app hear about changes the moment they happen, over a WebSocket, instead of polling.

Connect a standard graphql-ws client to the API host and pass your auth in connectionParams, as shown. Then start subscriptions the same way you write queries.

There are two styles of subscription:

  • Filter-aware (searchGraphChanged, searchRecommendationsChanged): you pass the same input as your query, and you only hear about items that match it. These are the ones to use for live lists.
  • Channel (everything else): coarse streams of every change to a kind of thing - every block update, every tag update. Simpler, chattier; you decide what to do with each event.

Many channel subscriptions take a tenantId argument. It is a routing filter, not a security check - every event is separately access-checked against your token before delivery, so you can only ever receive events about things you can see. Pass your own tenant._id.

Setupgraphql-ws
import { createClient } from 'graphql-ws';

const ws = createClient({
  url: 'wss://api.getnoded.ai/api/v1/graph',
  connectionParams: () => ({
    headers: { authorization: `Bearer ${token}` },
  }),
});

ws.subscribe(
  { query: SUBSCRIPTION, variables },
  { next: handleEvent, error: console.error,
    complete: () => {} }
);

searchGraphChanged

subscription

The live companion to searchGraph. Subscribe with the same input you queried with, and you receive an event whenever a block starts, stops, or continues matching that filter. Each event carries the block and a mutationType that tells you how to update your list:

  • Insert - a new match. Insert it into your list in sort order.
  • Update - an existing item changed. Replace it - and drop it if it no longer matches your filter.
  • Delete - it was archived or unshared. Remove it.

One practical tip from the Noded app: when your input uses search or folder scoping, skip the in-place patching and simply refetch the query - those filters can only be evaluated on the server.

Arguments

inputGraphSearchInputrequired

The same filter object as your searchGraph query.

Emits

SearchGraphChangedPayload: { block, mutationType }.

Request
subscription LiveActivity($input: GraphSearchInput!) {
  searchGraphChanged(input: $input) {
    mutationType
    block {
      _id
      blockType
      title
      dateCreated
    }
  }
}
Event
{
  "data": {
    "searchGraphChanged": {
      "mutationType": "Insert",
      "block": {
        "_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
        "blockType": "email",
        "title": "Re: pilot kickoff",
        "dateCreated": 1757083380000
      }
    }
  }
}

searchRecommendationsChanged

subscription

The live companion to searchRecommendations - the same pattern as searchGraphChanged, for the feed. Subscribe with your query's SearchInput and apply events by mutationType.

Arguments

inputSearchInput

The same filter object as your feed query.

Emits

{ recommendation, mutationType }.

Request
subscription LiveFeed($input: SearchInput) {
  searchRecommendationsChanged(input: $input) {
    mutationType
    recommendation {
      _id
      displayTitle
      recommendationType
    }
  }
}
Event
{
  "data": {
    "searchRecommendationsChanged": {
      "mutationType": "Insert",
      "recommendation": {
        "_id": "0f1a2b3c-4d5e-4f6a-8b7c-8d9e0f1a2b3c",
        "displayTitle": "Drafted follow-up tasks",
        "recommendationType": "UPSERT"
      }
    }
  }
}

Channel subscriptions

subscription

The remaining subscriptions are simple streams: subscribe once, and every change of that kind (that you are allowed to see) arrives as it happens. Each emits the changed object itself. The full list:

Channels

blockCreated / blockUpdated / blockArchived / blockChanged(tenantId!, blockType?)

Every block event, optionally filtered to one block type. blockChanged covers all three.

tagCreated / tagUpdated / tagArchived(tenantId!)

Tag lifecycle events.

folderCreated / folderUpdated / folderArchived(tenantId!)

Folder lifecycle events.

navigationUpdated(tenantId!)

The sidebar changed - emits the complete new NavigationTree.

recommendationCreated / recommendationUpdated / recommendationArchived / recommendationChanged(tenantId!)

Feed events - good for toasts on recommendationCreated.

messageCreatedForBlock / messageUpdatedForBlock / messageArchivedForBlock / messageChangedForBlock(blockId!)

Comments on one block - use while that block is open. message*ForTag(tagId!) variants exist too.

reactionAddedToBlock / reactionRemovedFromBlock / reactionChangedForBlock(blockId!)

Reactions on one block.

searchCreated / searchUpdated / searchArchived / searchChanged(tenantId!)

Saved-search events.

recentTagsUpdated(tenantId!)

The recent-tags list changed - emits the new list.

tagsWithActivityChanged(none)

Badge counts changed - emits the new list.

tenantUpdated(none)

The user's own profile or settings changed.

agentUpdated(none)

An agent's state changed - useful after triggerAgent.

Requesttoast on new feed items
subscription Toasts($tenantId: String!) {
  recommendationCreated(tenantId: $tenantId) {
    _id
    displayTitle
    displayDescription
  }
}
Event
{
  "data": {
    "recommendationCreated": {
      "_id": "0f1a2b3c-4d5e-4f6a-8b7c-8d9e0f1a2b3c",
      "displayTitle": "Meeting summary ready",
      "displayDescription": "Your 2pm call with Acme was transcribed and summarized."
    }
  }
}