# 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.

Source: https://nodaro.ai/docs/developers/sdk/workflows

**`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](https://nodaro.ai/docs/developers/api/workflows). See [Workflows and projects](https://nodaro.ai/docs/concepts/workflows-and-projects) for the concepts.

## Methods

| Method | What it does |
| --- | --- |
| [`workflows.list(params)`](#listparams) | List the workflows of a project, without their graphs |
| [`workflows.get(id)`](#getid) | Read one workflow with its nodes, connections and settings |
| [`workflows.getPublic(id)`](#getpublicid) | Read a workflow shared by link, without a token |
| [`workflows.create(input)`](#createinput) | Create a workflow in a project |
| [`workflows.update(id, input)`](#updateid-input) | Change any fields of a workflow |
| [`workflows.delete(id)`](#deleteid) | Delete a workflow |
| [`workflows.run(id, params?)`](#runid-params) | Start a run of the workflow |
| [`workflows.export(workflowId, opts?)`](#exportworkflowid-opts) | Export a workflow as a portable JSON bundle |
| [`workflows.import(input)`](#importinput) | Import a bundle into a project |
| [`workflows.setVisibility(id, visibility)`](#setvisibilityid-visibility) | Make a workflow private or visible to its workspace |
| [`workflows.move(id, params)`](#moveid-params) | Move a workflow to another project |
| [`workflows.sharedWithMe()`](#sharedwithme) | List workflows other people shared with you |
| [`workflows.collaborators.*`](#collaborators) | List, add, change and remove the people a workflow is shared with |
| [`projects.list()`](#projectslist) and more | List, 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.

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

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "The project whose workflows to list." },
}}
/>

```ts
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`.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "The workflow id." },
}}
/>

```ts
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`.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "The id of the shared workflow." },
}}
/>

```ts
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.

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

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "The project the workflow is created in." },
name: { type: 'string', required: true, description: "The workflow name." },
description: { type: 'string', description: "A description." },
folderId: { type: 'string | null', description: "The folder inside the project." },
nodes: { type: 'GenericNode[]', description: "The nodes on the canvas." },
edges: { type: 'GenericEdge[]', description: "The connections between nodes." },
settings: { type: 'Record<string, unknown>', description: "Workflow settings." },
sourcePrompt: { type: 'string', description: "The prompt the workflow was generated from, if any." },
}}
/>

```ts
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](https://nodaro.ai/docs/developers/sdk/apps-and-templates).

### 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.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "The workflow id." },
name: { type: 'string', description: "A new name." },
description: { type: 'string', description: "A new description." },
folderId: { type: 'string | null', description: "Move the workflow to another folder of the same project." },
nodes: { type: 'GenericNode[]', description: "The complete new list of nodes." },
edges: { type: 'GenericEdge[]', description: "The complete new list of connections." },
settings: { type: 'Record<string, unknown>', description: "New workflow settings." },
sourcePrompt: { type: 'string', description: "The prompt the workflow was generated from." },
thumbnailUrl: { type: 'string | null', description: "The preview image: the URL of an image that is already hosted, or null to remove it." },
expectedVersion: { type: 'number', description: "The version you read. When the stored version differs, the update is refused with WorkflowConflictError. Preferred over expectedUpdatedAt." },
expectedUpdatedAt: { type: 'string', description: "The updatedAt value you read, as an older alternative to expectedVersion." },
}}
/>

```ts
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`](https://nodaro.ai/docs/developers/sdk/errors#workflowconflicterror), whose `currentRecord` holds the current workflow so you can merge without another read:

```ts

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.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "The workflow id." },
}}
/>

```ts
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()`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) for its progress and result.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "The workflow id." },
nodeIds: { type: 'string[]', description: "Run only these nodes. Omit to run the whole workflow." },
}}
/>

```ts
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()`](https://nodaro.ai/docs/developers/sdk/client#withworkspaceworkspaceid).

### 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.

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

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "The workflow to export." },
assets: { type: 'boolean', default: 'false', description: "Include the characters, objects, creatures and locations the workflow uses, so they are recreated on import." },
}}
/>

```ts
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`:

```ts
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](https://nodaro.ai/docs/guides/import-export).

### import(input)

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

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

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "The project to import into." },
'...bundle': { type: 'WorkflowExport', required: true, description: "The fields of the exported bundle, spread into the input." },
}}
/>

```ts
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:

```ts
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.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "The workflow id." },
visibility: { type: '"private" | "workspace"', required: true, description: "Who can see the workflow." },
}}
/>

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

Only the creator or a workspace admin may change it; anyone else gets `ForbiddenError`. Workspaces exist in [organizations](https://nodaro.ai/docs/developers/sdk/organizations) on Nodaro Cloud.

### move(id, params)

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

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "The workflow id." },
projectId: { type: 'string', required: true, description: "The destination project." },
}}
/>

```ts
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.

```ts
sharedWithMe(): Promise<{ data: (Workflow & { grantedRole: "viewer" | "editor" })[] }>
```

```ts
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](https://nodaro.ai/docs/developers/sdk/organizations); elsewhere they throw `NotFoundError`.

```ts
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 }>
```

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "The workflow id." },
userId: { type: 'string', description: "The person's user id. In add(), give exactly one of userId and email." },
email: { type: 'string', description: "Any email address. The person does not need an account yet." },
role: { type: '"viewer" | "editor"', required: true, description: "What the person may do with the workflow." },
}}
/>

```ts
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.

```ts
list(): Promise<{ data: Project[] }>
```

```ts
const { data: projects } = await client.projects.list()
```

### projects.get(id)

Reads one project.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "The project id." },
}}
/>

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

### projects.create(input)

Creates a project.

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

<TypeTable
type={{
name: { type: 'string', required: true, description: "The project name." },
description: { type: 'string', description: "A description." },
settings: { type: 'Record<string, unknown>', description: "Project settings." },
}}
/>

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

### projects.update(id, input)

Changes a project. Pass at least one field.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "The project id." },
name: { type: 'string', description: "A new name." },
description: { type: 'string', description: "A new description." },
settings: { type: 'Record<string, unknown>', description: "New project settings." },
}}
/>

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

### projects.delete(id)

Deletes a project.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "The project id." },
}}
/>

```ts
await client.projects.delete(projectId)
```

## Types

| Type | Shape |
| --- | --- |
| `Workflow` | `id`, `projectId`, `userId`, `name`, `description?`, `folderId?`, `version?`, `thumbnailUrl?`, `nodes?`, `edges?`, `settings?`, `sourcePrompt?`, `createdAt`, `updatedAt`. The graph fields are present on full records only. |
| `Project` | `id`, `userId`, `name`, `description?`, `settings?`, `createdAt`, `updatedAt` |
| `Collaborator` | `userId`, `name?`, `avatar?`, `role` |
| `WorkflowExport` | The portable bundle that `export()` returns and `import()` accepts |
| `GenericNode`, `GenericEdge` | The node and connection shapes used in `nodes` and `edges` |

All of them are exported from `@nodaro/sdk`. See [Types](https://nodaro.ai/docs/developers/sdk/types).

## Frequently asked questions

### How do I run a Nodaro workflow from code?

Call client.workflows.run(workflowId). It returns an executionId right away. Poll client.executions.get(executionId) until the status is completed, failed, cancelled or timed_out.

### Can I run only some nodes of a workflow?

Yes. Pass an object with nodeIds, the list of node ids to run, as the second argument of client.workflows.run. Only those nodes run.

### How do I avoid overwriting someone else's changes?

Pass expectedVersion, the version you read, to client.workflows.update. When the workflow changed in the meantime, the call throws WorkflowConflictError with the current record, so you can merge and retry.

### How do I copy a workflow to another Nodaro instance?

Export it with client.workflows.export and the assets option set to true, then import the bundle on the other instance with client.workflows.import and a projectId. Reachable media is copied to the new instance's storage.
