# Workflows e projetos

> Crie, atualize, compartilhe, exporte, importe e execute workflows do Nodaro em TypeScript com client.workflows e agrupe-os em projetos com client.projects.

Source: https://nodaro.ai/pt-BR/docs/developers/sdk/workflows

**`client.workflows`** lê, grava, compartilha e executa workflows do Nodaro, e **`client.projects`** gerencia os projetos que os contêm. Um workflow é um canvas de nós conectados, e todo workflow pertence a um projeto. Os métodos chamam os mesmos endpoints da [API REST de workflows](https://nodaro.ai/docs/developers/api/workflows). Veja [Workflows e projetos](https://nodaro.ai/docs/concepts/workflows-and-projects) para os conceitos.

## Métodos
| Método | O que faz |
| --- | --- |
| [`workflows.list(params)`](#listparams) | Lista os workflows de um projeto, sem os grafos deles |
| [`workflows.get(id)`](#getid) | Lê um workflow com os nós, as conexões e as configurações dele |
| [`workflows.getPublic(id)`](#getpublicid) | Lê um workflow compartilhado por link, sem token |
| [`workflows.create(input)`](#createinput) | Cria um workflow em um projeto |
| [`workflows.update(id, input)`](#updateid-input) | Altera quaisquer campos de um workflow |
| [`workflows.delete(id)`](#deleteid) | Exclui um workflow |
| [`workflows.run(id, params?)`](#runid-params) | Inicia uma execução do workflow |
| [`workflows.export(workflowId, opts?)`](#exportworkflowid-opts) | Exporta um workflow como um pacote JSON portátil |
| [`workflows.import(input)`](#importinput) | Importa um pacote para um projeto |
| [`workflows.setVisibility(id, visibility)`](#setvisibilityid-visibility) | Torna um workflow privado ou visível para o espaço de trabalho dele |
| [`workflows.move(id, params)`](#moveid-params) | Move um workflow para outro projeto |
| [`workflows.sharedWithMe()`](#sharedwithme) | Lista os workflows que outras pessoas compartilharam com você |
| [`workflows.collaborators.*`](#collaborators) | Lista, adiciona, altera e remove as pessoas com quem um workflow é compartilhado |
| [`projects.list()`](#projectslist) e outros | Lista, lê, cria, atualiza e exclui projetos |

## client.workflows
### list(params)
Lista os workflows de um projeto. A lista retorna só metadados: `nodes`, `edges`, `settings` e `sourcePrompt` ficam de fora. Leia um workflow com `get()` para obter o grafo completo dele.

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

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "O projeto cujos workflows serão listados." },
}}
/>

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

Lança `NotFoundError` quando o projeto não está visível para você.

### get(id)
Lê um workflow, incluindo `nodes`, `edges` e `settings` completos.

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

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

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

Guarde a `version` se você pretende atualizar o workflow: ela torna a atualização segura contra outras pessoas que também gravam nele.

### getPublic(id)
Lê um workflow que o dono compartilhou por link (`GET /v1/public/workflows/:id`). Não é preciso token. O workflow só é retornado enquanto o compartilhamento dele estiver ativado; caso contrário, o método lança `NotFoundError`.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do workflow compartilhado." },
}}
/>

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

Quando o provedor de autenticação não retorna nenhum token, a requisição é enviada sem token.

### create(input)
Cria um workflow em um projeto e retorna o registro completo. Só `projectId` e `name` são necessários; os outros campos usam os padrões do servidor.

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

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "O projeto em que o workflow é criado." },
name: { type: 'string', required: true, description: "O nome do workflow." },
description: { type: 'string', description: "Uma descrição." },
folderId: { type: 'string | null', description: "A pasta dentro do projeto." },
nodes: { type: 'GenericNode[]', description: "Os nós no canvas." },
edges: { type: 'GenericEdge[]', description: "As conexões entre os nós." },
settings: { type: 'Record<string, unknown>', description: "As configurações do workflow." },
sourcePrompt: { type: 'string', description: "O prompt a partir do qual o workflow foi gerado, se houver." },
}}
/>

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

Para obter um grafo pronto, exporte um workflow que você montou no editor e leia os `nodes` e `edges` dele, ou clone um [template](https://nodaro.ai/docs/developers/sdk/apps-and-templates).

### update(id, input)
Altera qualquer subconjunto dos campos de um workflow e retorna o registro completo atualizado (`PATCH /v1/workflows/:id`). Os campos que você deixa de fora continuam como estão.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do workflow." },
name: { type: 'string', description: "Um novo nome." },
description: { type: 'string', description: "Uma nova descrição." },
folderId: { type: 'string | null', description: "Move o workflow para outra pasta do mesmo projeto." },
nodes: { type: 'GenericNode[]', description: "A nova lista completa de nós." },
edges: { type: 'GenericEdge[]', description: "A nova lista completa de conexões." },
settings: { type: 'Record<string, unknown>', description: "As novas configurações do workflow." },
sourcePrompt: { type: 'string', description: "O prompt a partir do qual o workflow foi gerado." },
thumbnailUrl: { type: 'string | null', description: "A imagem de prévia: a URL de uma imagem que já está hospedada, ou null para removê-la." },
expectedVersion: { type: 'number', description: "A versão que você leu. Quando a versão armazenada é diferente, a atualização é recusada com WorkflowConflictError. Preferível a expectedUpdatedAt." },
expectedUpdatedAt: { type: 'string', description: "O valor de updatedAt que você leu, como alternativa mais antiga a expectedVersion." },
}}
/>

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

**Atualizações seguras.** Sem `expectedVersion` ou `expectedUpdatedAt`, a última gravação prevalece. Com um deles, a atualização só é aplicada se ninguém tiver alterado o workflow desde que você o leu. Caso contrário, ela lança [`WorkflowConflictError`](https://nodaro.ai/docs/developers/sdk/errors#workflowconflicterror), cujo `currentRecord` contém o workflow atual para que você possa mesclar sem uma nova leitura:

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

Os valores de estado de execução nos dados dos nós, como o status de execução ou o ID do job atual, são removidos pelo servidor e nunca são salvos.

### delete(id)
Exclui um workflow.

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

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

```ts
await client.workflows.delete(workflowId)
```

Lança `NotFoundError` quando o ID não existe ou não é seu; assim, uma exclusão nunca falha em silêncio.

### run(id, params?)
Inicia uma execução do workflow e retorna na hora com `{ executionId, status }`. A execução continua no servidor. Consulte [`client.executions.get()`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) periodicamente para acompanhar o progresso e o resultado.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do workflow." },
nodeIds: { type: 'string[]', description: "Executa só estes nós. Omita para executar o workflow inteiro." },
}}
/>

```ts
const { executionId } = await client.workflows.run(workflowId, {
nodeIds: ["text-prompt-1", "image-gen-2"],
})
const { data: execution } = await client.executions.get(executionId)
```

- Lança `InsufficientCreditsError` quando a conta não consegue cobrir o maior custo possível da execução.
- Um token OAuth precisa do escopo `workflows:execute`.
- Para executar um workflow em um espaço de trabalho, chame `run` em um cliente obtido com [`client.withWorkspace()`](https://nodaro.ai/docs/developers/sdk/client#withworkspaceworkspaceid).

### export(workflowId, opts?)
Exporta a versão salva de um workflow como um pacote JSON portátil, o formato de arquivo que o menu **Exportar** do editor grava. Com `assets: true`, o pacote também leva as entidades que o workflow usa, como **Com mídias** no editor.

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

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "O workflow a exportar." },
assets: { type: 'boolean', default: 'false', description: "Inclui os personagens, objetos, criaturas e locais que o workflow usa, para que sejam recriados na importação." },
}}
/>

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

**Portabilidade.** Um pacote só pode apontar para mídias que outra instância consiga buscar. Quando os nós usam URLs que outras instâncias não conseguem acessar, como o armazenamento próprio de uma instalação self-hosted em `localhost`, um endereço de rede local ou um nome `.internal`, o pacote lista essas URLs em `portability.unreachableMedia`:

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

O campo não aparece quando todas as URLs de mídia são públicas. Um pacote assim ainda pode ser importado, mas esses nós não podem ser executados na outra instância até que as mídias sejam enviadas para lá de novo. Veja [Importação e exportação](https://nodaro.ai/docs/guides/import-export).

### import(input)
Importa um pacote exportado para um dos seus projetos e retorna o novo workflow.

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

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "O projeto de destino da importação." },
'...bundle': { type: 'WorkflowExport', required: true, description: "Os campos do pacote exportado, espalhados na entrada." },
}}
/>

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

O que a importação faz:

- **As entidades são recriadas.** Os personagens, objetos, criaturas e locais do pacote viram novos itens na sua conta. O workflow passa a apontar para eles nos nós de entidade, em cada menção com `@` nos dados dos nós e nas configurações do workflow.
- **As mídias são copiadas.** As mídias em outros hosts são copiadas para o armazenamento desta instância quando podem ser acessadas; assim, o workflow não depende do servidor de outra pessoa. Os limites são 25 arquivos para as mídias do workflow e mais 25 para as entidades do pacote, com imagens de até 20 MB e vídeo ou áudio de até 50 MB.
- **As cópias contam no seu armazenamento.** Quando o armazenamento acaba, o workflow ainda é importado, e as entidades que não couberam são listadas no relatório.

O `importReport` informa o que aconteceu:

```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` está presente sempre que o pacote trouxe entidades. O servidor já atualizou todas as menções dentro do workflow. Use o mapa só para referências que você guarda fora dele. `assetsSkipped` só aparece quando algo ficou de fora.

### setVisibility(id, visibility)
Torna um workflow `"private"`, visível para quem o criou e para os colaboradores que você adicionar, ou `"workspace"`, visível para todos no espaço de trabalho dele.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do workflow." },
visibility: { type: '"private" | "workspace"', required: true, description: "Quem pode ver o workflow." },
}}
/>

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

Só quem criou o workflow ou um administrador do espaço de trabalho pode mudar isso; qualquer outra pessoa recebe `ForbiddenError`. Os espaços de trabalho existem nas [organizações](https://nodaro.ai/docs/developers/sdk/organizations) do Nodaro Cloud.

### move(id, params)
Move um workflow para outro projeto (`POST /v1/workflows/:id/move`). O workflow sai da pasta em que estava.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do workflow." },
projectId: { type: 'string', required: true, description: "O projeto de destino." },
}}
/>

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

Quando a mudança tira o workflow de um espaço de trabalho, o acesso que vinha desse espaço de trabalho termina. Essas pessoas são retornadas em `droppedCollaborators`.

### sharedWithMe()
Lista os workflows que outras pessoas compartilharam diretamente com você. O que está em um espaço de trabalho do qual você faz parte não é incluído, porque já aparece nas listas desse espaço de trabalho. Cada workflow traz o `grantedRole` que você tem nele.

```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)
```

A lista fica vazia em instâncias sem organizações.

### collaborators
As pessoas com quem um workflow é compartilhado, acessadas como `client.workflows.collaborators`. Estes métodos existem em instâncias com [organizações](https://nodaro.ai/docs/developers/sdk/organizations); nas outras, eles lançam `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: "O ID do workflow." },
userId: { type: 'string', description: "O ID de usuário da pessoa. Em add(), informe exatamente um entre userId e email." },
email: { type: 'string', description: "O endereço de e-mail de qualquer conta do Nodaro, seja qual for a organização dela. Um endereço sem conta lança NotFoundError." },
role: { type: '"viewer" | "editor"', required: true, description: "O que a pessoa pode fazer com o 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
```

Um colaborador pode chamar `remove()` com o próprio ID de usuário para sair de um workflow.

## client.projects
Um projeto agrupa workflows. Todo workflow pertence a exatamente um projeto.

### projects.list()
Lista seus projetos.

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

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

### projects.get(id)
Lê um projeto.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do projeto." },
}}
/>

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

### projects.create(input)
Cria um projeto.

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

<TypeTable
type={{
name: { type: 'string', required: true, description: "O nome do projeto." },
description: { type: 'string', description: "Uma descrição." },
settings: { type: 'Record<string, unknown>', description: "As configurações do projeto." },
}}
/>

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

### projects.update(id, input)
Altera um projeto. Passe pelo menos um campo.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do projeto." },
name: { type: 'string', description: "Um novo nome." },
description: { type: 'string', description: "Uma nova descrição." },
settings: { type: 'Record<string, unknown>', description: "As novas configurações do projeto." },
}}
/>

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

### projects.delete(id)
Exclui um projeto.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "O ID do projeto." },
}}
/>

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

## Tipos
| Tipo | Formato |
| --- | --- |
| `Workflow` | `id`, `projectId`, `userId`, `name`, `description?`, `folderId?`, `version?`, `thumbnailUrl?`, `nodes?`, `edges?`, `settings?`, `sourcePrompt?`, `createdAt`, `updatedAt`. Os campos do grafo só aparecem em registros completos. |
| `Project` | `id`, `userId`, `name`, `description?`, `settings?`, `createdAt`, `updatedAt` |
| `Collaborator` | `userId`, `name?`, `avatar?`, `role` |
| `WorkflowExport` | O pacote portátil que `export()` retorna e `import()` aceita |
| `GenericNode`, `GenericEdge` | Os formatos de nó e de conexão usados em `nodes` e `edges` |

Todos eles são exportados de `@nodaro/sdk`. Veja [Tipos](https://nodaro.ai/docs/developers/sdk/types).

## Frequently asked questions

### Como executo um workflow do Nodaro pelo código?

Chame client.workflows.run(workflowId). O método retorna um executionId na hora. Consulte client.executions.get(executionId) periodicamente até o status ser completed, failed, cancelled ou timed_out.

### Posso executar só alguns nós de um workflow?

Sim. Passe um objeto com nodeIds, a lista de IDs dos nós a executar, como segundo argumento de client.workflows.run. Só esses nós são executados.

### Como evito sobrescrever as alterações de outra pessoa?

Passe expectedVersion, a versão que você leu, para client.workflows.update. Se o workflow mudou nesse meio-tempo, a chamada lança WorkflowConflictError com o registro atual; assim, você pode mesclar e tentar de novo.

### Como copio um workflow para outra instância do Nodaro?

Exporte-o com client.workflows.export e a opção assets definida como true. Depois, importe o pacote na outra instância com client.workflows.import e um projectId. As mídias acessíveis são copiadas para o armazenamento da nova instância.
