Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
TypeScript SDK

Workflows and projects

Create, update, share, export, import and run Nodaro workflows from TypeScript with client.workflows, and organize them in projects with client.projects.

client.workflows reads, writes, shares and runs Nodaro workflows, and client.projects manages the projects that hold them. A workflow is a canvas of connected nodes, and every workflow belongs to one project. The methods call the same endpoints as the Workflows REST API. See Workflows and projects for the concepts.

Methods

MethodWhat it does
workflows.list(params)List the workflows of a project, without their graphs
workflows.get(id)Read one workflow with its nodes, connections and settings
workflows.getPublic(id)Read a workflow shared by link, without a token
workflows.create(input)Create a workflow in a project
workflows.update(id, input)Change any fields of a workflow
workflows.delete(id)Delete a workflow
workflows.run(id, params?)Start a run of the workflow
workflows.export(workflowId, opts?)Export a workflow as a portable JSON bundle
workflows.import(input)Import a bundle into a project
workflows.setVisibility(id, visibility)Make a workflow private or visible to its workspace
workflows.move(id, params)Move a workflow to another project
workflows.sharedWithMe()List workflows other people shared with you
workflows.collaborators.*List, add, change and remove the people a workflow is shared with
projects.list() and moreList, read, create, update and delete projects

client.workflows

list(params)

Lists the workflows in a project. The list returns metadata only: nodes, edges, settings and sourcePrompt are left out. Read one workflow with get() for its full graph.

list(params: { projectId: string }): Promise<{ data: Workflow[] }>

Prop

Type

const { data: workflows } = await client.workflows.list({ projectId })
for (const wf of workflows) console.log(wf.id, wf.name, wf.updatedAt)

Throws NotFoundError when the project is not visible to you.

get(id)

Reads one workflow, including its full nodes, edges and settings.

get(id: string): Promise<{ data: Workflow }>

Prop

Type

const { data: wf } = await client.workflows.get(workflowId)
console.log(`${wf.name}: ${wf.nodes?.length ?? 0} nodes, version ${wf.version}`)

Keep version if you plan to update the workflow: it makes the update safe against other writers.

getPublic(id)

Reads a workflow that its owner shared by link (GET /v1/public/workflows/:id). No token is needed. The workflow is returned only while its sharing is switched on; otherwise the method throws NotFoundError.

getPublic(id: string): Promise<{ data: Workflow }>

Prop

Type

const { data: shared } = await client.workflows.getPublic(workflowId)

When the auth provider returns no token, the request is sent without one.

create(input)

Creates a workflow in a project and returns the full record. Only projectId and name are needed; the other fields take server defaults.

create(input: CreateWorkflowInput): Promise<{ data: Workflow }>

Prop

Type

const { data: wf } = await client.workflows.create({
  projectId,
  name: "Product launch video",
  nodes: [],
  edges: [],
})

To get a ready-made graph, export a workflow you built in the editor and read its nodes and edges, or clone a template.

update(id, input)

Changes any subset of a workflow's fields and returns the full updated record (PATCH /v1/workflows/:id). Fields you leave out stay as they are.

update(id: string, input: UpdateWorkflowInput): Promise<{ data: Workflow }>

Prop

Type

await client.workflows.update(workflowId, { name: "Renamed", expectedVersion: 7 })

Safe updates. Without expectedVersion or expectedUpdatedAt, the last write wins. With one of them, the update applies only if nobody changed the workflow since you read it. Otherwise it throws WorkflowConflictError, whose currentRecord holds the current workflow so you can merge without another read:

import { WorkflowConflictError } from "@nodaro/sdk"

try {
  await client.workflows.update(workflowId, { settings, expectedVersion: wf.version })
} catch (err) {
  if (err instanceof WorkflowConflictError && err.currentRecord) {
    const merged = mergeSettings(err.currentRecord.settings, settings)
    await client.workflows.update(workflowId, {
      settings: merged,
      expectedVersion: err.currentVersion,
    })
  } else {
    throw err
  }
}

Run-state values on node data, such as the running status or the current job id, are removed by the server and never saved.

delete(id)

Deletes a workflow.

delete(id: string): Promise<{ success: true }>

Prop

Type

await client.workflows.delete(workflowId)

Throws NotFoundError when the id does not exist or is not yours, so a delete never fails silently.

run(id, params?)

Starts a run of the workflow and returns at once with { executionId, status }. The run continues on the server. Poll client.executions.get() for its progress and result.

run(id: string, params?: { nodeIds?: string[] }): Promise<{ executionId: string; status: "pending" | "running" }>

Prop

Type

const { executionId } = await client.workflows.run(workflowId, {
  nodeIds: ["text-prompt-1", "image-gen-2"],
})
const { data: execution } = await client.executions.get(executionId)
  • Throws InsufficientCreditsError when the account cannot cover the run's highest possible cost.
  • An OAuth token needs the workflows:execute scope.
  • To run a workflow in a workspace, call run on a client from client.withWorkspace().

export(workflowId, opts?)

Exports the saved version of a workflow as a portable JSON bundle, the file format that the editor's Export menu writes. With assets: true, the bundle also carries the assets the workflow uses, like With Assets in the editor.

export(workflowId: string, opts?: { assets?: boolean }): Promise<{ data: WorkflowExport }>

Prop

Type

const { data: bundle } = await client.workflows.export(workflowId, { assets: true })

Portability. A bundle can only point at media another instance can fetch. When nodes use URLs that other instances cannot reach, such as a self-hosted install's own storage on localhost, a local network address or an .internal name, the bundle lists them under portability.unreachableMedia:

bundle.portability?.unreachableMedia
// [{ nodeId: "n1", nodeLabel: "Video URL", field: "videoUrl", url: "http://localhost:3000/storage/..." }]

The field is absent when every media URL is public. Such a bundle still imports, but those nodes cannot run on the other instance until the media is uploaded there again. See Import and export.

import(input)

Imports an exported bundle into one of your projects and returns the new workflow.

import(input: WorkflowExport & { projectId: string }): Promise<{
  data: Workflow
  importReport?: WorkflowImportReport
}>

Prop

Type

const { data: wf, importReport } = await client.workflows.import({ ...bundle, projectId })

What the import does:

  • Assets are recreated. Characters, objects, creatures and locations in the bundle become new items in your account. The workflow is re-pointed at them, in its asset nodes, in every @ mention in node data, and in the workflow's settings.
  • Media is copied. Media on other hosts is copied into this instance's storage when it can be reached, so the workflow does not depend on someone else's server. The limits are 25 files for the workflow's media and 25 more for the bundled assets, with images up to 20 MB and video or audio up to 50 MB.
  • Copies count against your storage. When your storage runs out, the workflow still lands, and the assets that did not fit are listed in the report.

The importReport says what happened:

importReport
// {
//   rehosted: 3,                                    // files copied to this instance
//   unreachable: [{ nodeId, nodeLabel, field, url }], // private hosts, left as they were
//   skipped: [{ nodeId, field, url, reason: "HTTP 404" }],
//   assetIdMap: { "<bundled asset id>": "<new asset id>" },
//   assetsSkipped: [{ kind: "character", id, name: "Kira", reason: "Storage limit exceeded" }],
// }

assetIdMap is present whenever the bundle carried assets. The server has already updated every mention inside the workflow. Use the map only for references you keep outside it. assetsSkipped appears only when something was left out.

setVisibility(id, visibility)

Makes a workflow "private", visible to its creator and the collaborators you add, or "workspace", visible to everyone in its workspace.

setVisibility(id: string, visibility: "private" | "workspace"): Promise<{ data: Workflow }>

Prop

Type

await client.workflows.setVisibility(workflowId, "workspace")

Only the creator or a workspace admin may change it; anyone else gets ForbiddenError. Workspaces exist in organizations on Nodaro Cloud.

move(id, params)

Moves a workflow to another project (POST /v1/workflows/:id/move). The workflow leaves its folder.

move(id: string, params: { projectId: string }): Promise<{
  data: Workflow
  droppedCollaborators: { userId: string; name: string | null }[]
}>

Prop

Type

const { droppedCollaborators } = await client.workflows.move(workflowId, { projectId: archiveProjectId })

When the move takes the workflow out of a workspace, the access that came from that workspace ends. Those people are returned in droppedCollaborators.

sharedWithMe()

Lists workflows other people shared with you directly. Work in a workspace you belong to is not included, because it already appears in that workspace's own lists. Each workflow carries the grantedRole you hold on it.

sharedWithMe(): Promise<{ data: (Workflow & { grantedRole: "viewer" | "editor" })[] }>
const { data: shared } = await client.workflows.sharedWithMe()
for (const wf of shared) console.log(wf.name, wf.grantedRole)

The list is empty on instances without organizations.

collaborators

The people a workflow is shared with, reached as client.workflows.collaborators. These methods exist on instances with organizations; elsewhere they throw NotFoundError.

collaborators.list(workflowId: string): Promise<{ data: Collaborator[] }>
collaborators.add(workflowId: string, input: { userId?: string; email?: string; role: "viewer" | "editor" }): Promise<{ data: { userId: string; role: "viewer" | "editor" } }>
collaborators.update(workflowId: string, userId: string, input: { role: "viewer" | "editor" }): Promise<{ data: { userId: string; role: "viewer" | "editor" } }>
collaborators.remove(workflowId: string, userId: string): Promise<{ success: true }>

Prop

Type

await client.workflows.collaborators.add(workflowId, { email: "editor@example.com", role: "editor" })

const { data: people } = await client.workflows.collaborators.list(workflowId)
// [{ userId, name, avatar, role }]: email addresses are never returned

A collaborator can call remove() with their own user id to leave a workflow.

client.projects

A project groups workflows. Every workflow belongs to exactly one project.

projects.list()

Lists your projects.

list(): Promise<{ data: Project[] }>
const { data: projects } = await client.projects.list()

projects.get(id)

Reads one project.

get(id: string): Promise<{ data: Project }>

Prop

Type

const { data: project } = await client.projects.get(projectId)

projects.create(input)

Creates a project.

create(input: { name: string; description?: string; settings?: Record<string, unknown> }): Promise<{ data: Project }>

Prop

Type

const { data: project } = await client.projects.create({ name: "Spring campaign" })

projects.update(id, input)

Changes a project. Pass at least one field.

update(id: string, input: { name?: string; description?: string; settings?: Record<string, unknown> }): Promise<{ data: Project }>

Prop

Type

await client.projects.update(projectId, { description: "Assets for the spring launch" })

projects.delete(id)

Deletes a project.

delete(id: string): Promise<{ success: true }>

Prop

Type

await client.projects.delete(projectId)

Types

TypeShape
Workflowid, projectId, userId, name, description?, folderId?, version?, thumbnailUrl?, nodes?, edges?, settings?, sourcePrompt?, createdAt, updatedAt. The graph fields are present on full records only.
Projectid, userId, name, description?, settings?, createdAt, updatedAt
CollaboratoruserId, name?, avatar?, role
WorkflowExportThe portable bundle that export() returns and import() accepts
GenericNode, GenericEdgeThe node and connection shapes used in nodes and edges

All of them are exported from @nodaro/sdk. See Types.

Frequently asked questions

Last updated on

On this page