# 워크플로

> API로 저장된 Nodaro 워크플로를 새 입력값으로 실행하고 결과를 기다리거나 폴링하세요. 워크플로 목록, 생성, 수정, 내보내기, 이동도 다룹니다.

Source: https://nodaro.ai/ko/docs/developers/api/workflows

**워크플로**는 연결된 노드로 이루어진, 저장된 캔버스입니다. API로 워크플로를 실행하고, 한 번의 실행에만 새 입력값을 넣고, 다른 리소스처럼 관리할 수 있습니다. 실행은 `executionId`를 반환하는 요청 하나로 시작되며, 실행이 끝날 때까지 폴링합니다. 짧은 실행은 같은 요청 안에서 결과를 기다릴 수도 있습니다.

## 엔드포인트
### 워크플로 실행
| 메서드 | 경로 | 기능 |
| --- | --- | --- |
| `POST` | `/v1/workflows/:id/run` | 저장된 워크플로 또는 그 일부 노드를 실행합니다. `executionId`와 함께 `202`를 반환합니다. |
| `POST` | `/v1/workflows/:id/render-final/estimate` | 최종 렌더링의 견적입니다. 실행할 노드와 크레딧을 알려 줍니다. 아무것도 만들거나 청구하지 않습니다. |
| `GET` | `/v1/api/workflows` | 내 API 토큰으로 실행할 수 있는 워크플로 목록입니다. `?limit=`와 `?cursor=`를 지원합니다. |
| `GET` | `/v1/api/schema?workflowId=…` | 워크플로의 입력 필드와 출력, 그리고 `estimatedCredits`입니다. |
| `POST` | `/v1/api/run` | 새 입력값으로 워크플로를 실행합니다. 결과를 기다리려면 `?wait=true&timeout=…` 쿼리를 추가하세요. |
| `GET` | `/v1/api/status/:execId` | 실행의 상태, 노드 수, 사용한 크레딧, 그리고 완료된 뒤에는 `outcome`입니다. |
| `GET` | `/v1/api/result/:execId` | 상태가 `completed` 또는 `failed`가 된 실행의 출력입니다. |
| `POST` | `/v1/app/:slug/run` | 게시된 앱을 그 폼 필드로 실행합니다. [미니앱](https://nodaro.ai/docs/developers/embed/miniapps)을 참고하세요. |

### 워크플로 관리
| 메서드 | 경로 | 기능 |
| --- | --- | --- |
| `GET` | `/v1/projects/:projectId/workflows` | 프로젝트 하나의 워크플로를 노드와 엣지 없이 반환합니다. |
| `GET` | `/v1/workflows` | 모든 프로젝트에 걸친 내 워크플로입니다. |
| `GET` | `/v1/workflows/:id` | 워크플로 하나를 노드, 엣지, 설정과 함께 반환합니다. |
| `POST` | `/v1/projects/:projectId/workflows` | 프로젝트에 워크플로를 만듭니다. |
| `PATCH` | `/v1/workflows/:id` | 워크플로의 필드를 원하는 만큼 골라 변경합니다. |
| `DELETE` | `/v1/workflows/:id` | 워크플로를 삭제합니다. |
| `GET` | `/v1/workflows/:id/export` | 워크플로를 JSON 번들로 내보냅니다. 캐릭터, 오브젝트, 장소를 포함하려면 `?assets=true`를 추가하세요. |
| `POST` | `/v1/workflows/import` | 번들로 워크플로를 만듭니다. |
| `POST` | `/v1/workflows/:id/move` | 워크플로를 다른 프로젝트로 옮깁니다. |
| `GET` | `/v1/workflows/shared-with-me` | 다른 사람이 나와 공유한 워크플로입니다. |

공유, 공동 작업자, 워크플로별 권한은 [워크스페이스와 조직](https://nodaro.ai/docs/developers/api/workspaces)에서 다룹니다.

## 워크플로를 실행하는 세 가지 방법
| 엔드포인트 | 자격 증명 | 입력값 | 사용 시기 |
| --- | --- | --- | --- |
| `POST /v1/workflows/:id/run` | 모든 토큰. OAuth 토큰에는 `workflows:execute`가 필요합니다. | 저장된 값. `nodeIds`로 일부만 실행합니다. | 내 계정이나 OAuth 사용자를 위해 저장된 그대로 워크플로를 실행할 때. |
| `POST /v1/api/run` | 개인 API 토큰 | `inputs`가 이번 실행의 입력 노드 값을 바꿉니다. | 스크립트가 실행할 때마다 다른 값을 써야 하거나 결과를 기다리려 할 때. |
| `POST /v1/app/:slug/run` | 모든 토큰 | 앱의 폼 필드와 노드 원시 재정의 값 | 워크플로가 다듬어진 폼을 갖춘 앱으로 게시되어 있을 때. |

다섯 개의 `/v1/api/` 엔드포인트는 원래의 API 토큰 전용 경로입니다. 게시된 앱보다 먼저 만들어졌고 계속 지원되지만, 입력이 필요한 새 연동은 보통 워크플로를 앱으로 게시하고 `POST /v1/app/:slug/run`을 사용합니다. 이 라우트는 앱의 필드를 평평한 `inputs`로 받고, 원시 `{ nodeId: { field: value } }` 재정의 값으로 이루어진 `inputOverrides` 객체를 선택적으로 받습니다. 두 값은 병합되며, 두 곳에서 같은 필드를 설정하면 `inputOverrides`가 우선합니다. 노출된 슬라이더처럼 숫자 필드인 평평한 입력은 숫자나 숫자 문자열을 받습니다. `"0.4"`는 노드에 `0.4`로 도착합니다. 숫자가 아닌 값은 그대로 전달되고, 노드는 그 값을 늘 하던 대로 처리합니다.

## 저장된 워크플로 실행
`POST /v1/workflows/:id/run`은 저장된 그대로 워크플로의 실행을 시작합니다. 모든 노드를 실행하려면 빈 본문을 보내고, 일부만 실행하려면 `nodeIds`를 보내세요.

**curl**

```bash
curl -s -X POST https://app.nodaro.ai/v1/workflows/8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f/run \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{ "executionId": "3f9e2b1a-7c6d-4e5f-8a9b-0c1d2e3f4a5b", "status": "pending" }
```

**TypeScript SDK**

```ts
const { executionId } = await client.workflows.run(workflowId)

// Or run only some nodes:
await client.workflows.run(workflowId, { nodeIds: ['text-prompt-1', 'generate-image-1'] })
```

**CLI**

```bash
nodaro workflows run 8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f --watch
nodaro workflows run 8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f --node text-prompt-1 generate-image-1
```

<TypeTable
type={{
nodeIds: { type: 'string[]', description: '선택 사항. 실행할 노드의 ID입니다. 생략하면 워크플로 전체를 실행합니다.' },
inputOverrides: { type: 'object', description: '선택 사항. 이번 실행에만 쓰는 입력 노드의 값으로, 노드 ID를 키로 합니다. 예: { "text-prompt-1": { "text": "a cat at sunset" } }. 노드 ID에 값만 넣으면 그 노드의 기본 필드에 들어갑니다. 여기서는 노드 레이블을 쓸 수 없습니다.' },
continueFromExecutionId: { type: 'string', description: '선택 사항. 이어갈 이 워크플로의 완료된 실행입니다. nodeIds만 실행되고, 다른 모든 노드는 그 실행이 만든 결과를 넘겨줍니다. 실행 이어가기를 참고하세요.' },
renderFinal: { type: '{ renderNodeId: string }', description: '선택 사항. EDL 적용 노드를 최종 품질로 렌더링한 다음, 그 뒤의 모든 노드를 실행합니다. continueFromExecutionId와 함께 보내고, nodeIds나 inputOverrides는 보내지 마세요. 최종 렌더링을 참고하세요.' },
}}
/>

응답은 `{ executionId, status }`를 담은 `202 Accepted`이며, `status`는 `pending` 또는 `running`입니다. 실행은 `GET /v1/workflow-executions/:id`로 폴링하세요. 자세한 내용은 [실행](https://nodaro.ai/docs/developers/api/executions)에 있습니다. `--watch`를 쓰면 CLI가 대신 폴링하고, 실행이 실패하면 코드 `2`로, 취소되면 코드 `130`으로 종료합니다.

| 상태 | 코드 | 의미 |
| --- | --- | --- |
| 402 | `insufficient_credits` | 내 크레딧으로 감당할 수 없는 최종 렌더링입니다. 아무것도 만들어지지 않았습니다. |
| 403 | `forbidden` | 워크플로를 볼 수는 있지만 실행할 수는 없습니다. 워크스페이스에서 실행하려면 편집 권한과 활성 멤버십이 필요합니다. |
| 404 | `not_found` | 그런 워크플로가 없거나, 내가 볼 수 있는 워크플로가 아닙니다. |
| 409 | `already_running` | 워크플로에 이미 진행 중인 실행이 있습니다. 응답에 그 실행의 `executionId`가 담깁니다. |

일반 실행은 시작하기 전에 잔액을 확인하지 않습니다. 크레딧이 어떤 노드의 비용을 감당하지 못하면 그 노드가 크레딧 부족 오류로 실패하고, 실행은 그 실패를 보고합니다.

### 실행 이어가기
`POST /v1/workflows/:id/run`은 처음부터 다시 시작하는 대신 이전 실행을 이어갈 수 있습니다. 그 실행의 ID를 `continueFromExecutionId`에 넣고 `nodeIds`와 함께 보내세요. 이 실행은 `nodeIds`만 실행합니다. 다른 모든 노드는 워크플로에 저장된 결과가 아니라, 이전 실행이 기록한 결과를 그대로 넘겨줍니다. 업로드나 앱 입력처럼 그 실행이 사용한 입력이 다시 쓰이고, `nodeIds`보다 앞의 노드는 다시 실행되거나 청구되지 않습니다.

- 이전 실행에서도 실행되지 않은 노드는 그 실행 때처럼 저장된 결과를 넘겨줍니다.
- [편집 계획](https://nodaro.ai/docs/nodes/video/edit-plan)은 이전 실행이 만든 계획에 지금의 검토를 적용해 넘겨줍니다. 다른 계획에 대해 한 검토는 무시됩니다.
- 이전 실행이 완료하지 못한 노드는 아무것도 넘겨주지 않고, 다시 실행되지도 않습니다. 모든 입력이 이런 노드에서 오는 노드는 그 뒤의 노드와 함께 건너뜁니다.
- 이전 실행의 `inputOverrides`가 다시 적용됩니다. 내가 보낸 `inputOverrides`는 그 위에 필드별로 적용되며, 내 값이 우선합니다.

직접 작성한 최종 렌더링이 한 예입니다. 렌더 노드와 그 뒤의 모든 노드를 실행하며, 이번 실행에서만 렌더를 최종 품질로 설정합니다.

```json
{
"nodeIds": ["<render node id>", "<every node after it>"],
"inputOverrides": { "<render node id>": { "quality": "final" } },
"continueFromExecutionId": "<a completed run whose render made a Preview>"
}
```

이전 실행은 내가 시작한 실행이어야 하고, 게시된 앱 버전이 아니라 이 워크플로 자체의 실행이어야 하며, `completed`로 끝났어야 합니다. 그렇지 않으면 실행이 만들어지기 전에 요청이 거부됩니다.

| 상태 | 코드 | 의미 |
| --- | --- | --- |
| 400 | `continuation_subset_required` | `nodeIds`가 없거나 비어 있습니다. |
| 400 | `continuation_workflow_mismatch` | 이전 실행이 다른 워크플로를 실행했습니다. |
| 400 | `continuation_version_mismatch` | 이전 실행이 워크플로의 게시된 앱 버전을 실행했습니다. |
| 400 | `validation_error` | `continueFromExecutionId`가 실행 ID가 아닙니다. |
| 404 | `continuation_not_found` | 그런 실행이 없거나 내 실행이 아닙니다. |
| 409 | `continuation_not_completed` | 이전 실행이 아직 진행 중이거나, 실패했거나, 취소되었습니다. |

### 최종 렌더링
노드를 직접 나열하는 대신, 렌더 노드를 지정하고 나머지는 서버에 맡길 수 있습니다. 렌더 노드의 ID를 담은 `renderFinal`을 `continueFromExecutionId`와 함께 보내고, `nodeIds`와 `inputOverrides`는 보내지 마세요. 서버는 편집기의 **최종 렌더링**과 같은 것을 실행합니다. 이번 실행에서만 [EDL 적용](https://nodaro.ai/docs/nodes/video/apply-edl) 렌더를 최종 품질로 실행하고, 그 뒤의 모든 노드를 실행합니다. 편집 계획과 렌더 사이에 [카메라 전환](https://nodaro.ai/docs/nodes/video/camera-switch)이 있으면 카메라 전환이 먼저 다시 실행됩니다. 편집 계획은 다시 실행되지 않습니다. 그런 다음 이 실행은 같은 검사를 거치는 이어가기 실행이 됩니다.

```json
{
"renderFinal": { "renderNodeId": "<render node id>" },
"continueFromExecutionId": "<a completed run whose render made a Preview>"
}
```

먼저 `POST /v1/workflows/:id/render-final/estimate`에 `{ renderNodeId, continueFromExecutionId }` 본문을 보내 견적을 받으세요. 견적은 아무것도 만들거나 청구하지 않습니다. 응답은 `200`과 `{ data: { renderNodeId, nodeIds, inputOverrides, estimatedCredits, sufficient, available } }`입니다.

- `nodeIds`와 `inputOverrides`는 실행이 실행할 노드와 실행에 쓰는 재정의 값 `{ "<render node id>": { "quality": "final" } }`입니다.
- `estimatedCredits`는 렌더를 최종 품질로 한, 실행할 노드들의 가격입니다.
- `sufficient`는 내 크레딧으로 그 가격을 감당할 수 있는지 알려 주고, `available`은 사용할 수 있는 내 크레딧입니다. 워크스페이스 예산이나 배포의 결제 계정이 비용을 내면 `available`은 `null`입니다.
- 크레딧이 없는 에디션에서는 세 값 모두 `null`입니다.

견적은 위의 이어가기 코드로 거부됩니다. 두 라우트는 다음 코드로도 거부됩니다.

| 상태 | 코드 | 의미 |
| --- | --- | --- |
| 400 | `render_final_node_not_found` | 워크플로에 그 ID의 노드가 없습니다. |
| 400 | `render_final_not_a_render` | 그 노드가 EDL 적용 렌더가 아닙니다. |
| 400 | `validation_error` | `renderFinal`을 `nodeIds`나 `inputOverrides`와 함께 보냈거나, `continueFromExecutionId` 없이 보냈습니다. |

최종 렌더링은 시작하기 전에, 견적에 표시된 금액을 기준으로 잔액을 확인합니다. 감당할 수 없는 실행은 `required`, `available`과 함께 `402 insufficient_credits`로 응답하며, 실행은 만들어지지 않습니다. 이 확인은 잔액만 봅니다. 내 요금제에서 쓸 수 없는 모델이나 일일 한도는 실행이 그 노드에 도달했을 때 그 노드에서 거부됩니다.

## 새 입력값으로 워크플로 실행
`POST /v1/api/run`은 이번 한 번의 실행에서 입력 노드의 값을 바꾸는 `inputs` 객체를 받습니다. 인증에는 개인 API 토큰을 사용합니다. `inputs`의 키는 노드 ID이며, 편의상 고유한 노드 레이블도 쓸 수 있습니다. 각 키 안에는 텍스트 프롬프트의 `text`처럼 노드의 입력 필드 이름을 넣으세요.

Workflow: 예시 워크플로: API 실행이 텍스트를 바꾸는 텍스트 노드가 이미지 생성 노드로 이어집니다.

- 텍스트 → 이미지 생성 (프롬프트)

아래 예시는 ID가 `text-prompt-1`인 [텍스트](https://nodaro.ai/docs/nodes/automate/text) 노드를 [이미지 생성](https://nodaro.ai/docs/nodes/image/generate-image) 노드에 연결한 워크플로를 실행합니다.

### 입력 필드 찾기
`GET /v1/api/schema`는 워크플로의 입력 목록을 각 입력이 받는 필드, 출력, 실행 한 번에 드는 크레딧 추정치와 함께 반환합니다.

```bash
curl -s "https://app.nodaro.ai/v1/api/schema?workflowId=$WORKFLOW_ID" \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

```json
{
"workflowId": "8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f",
"name": "Sunset stills",
"estimatedCredits": 50,
"inputs": [
{ "nodeId": "text-prompt-1", "key": "text", "label": "Prompt", "type": "text" }
],
"outputs": [
{ "nodeId": "generate-image-1", "label": "Generate Image", "type": "image" }
]
}
```

### 실행 시작
```bash
curl -s -X POST https://app.nodaro.ai/v1/api/run \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"workflowId": "8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f",
"inputs": {
"text-prompt-1": { "text": "a cat at sunset" }
}
}'
```

```json
{ "executionId": "3f9e2b1a-7c6d-4e5f-8a9b-0c1d2e3f4a5b", "status": "pending" }
```

### 실행이 끝날 때까지 폴링
`GET /v1/api/status/:execId`는 상태, 노드 수, 지금까지 사용한 크레딧을 반환합니다. 상태가 `completed`, `failed`, `cancelled`, `timed_out`, `discarded` 중 하나가 될 때까지 2~5초마다 폴링하세요.

### 결과 가져오기
`GET /v1/api/result/:execId`는 `completed` 또는 `failed`로 끝난 실행의 출력을 반환합니다. `cancelled`, `timed_out`, `discarded`로 끝난 실행은 대신 상태 응답에서 `errorMessage`를 읽으세요. 이런 실행에는 결과 페이로드가 없으며, 결과 라우트는 계속 `202`를 반환합니다.

```json
{
"executionId": "3f9e2b1a-7c6d-4e5f-8a9b-0c1d2e3f4a5b",
"status": "completed",
"outcome": "succeeded",
"creditsUsed": 50,
"durationMs": 12450,
"errorMessage": null,
"outputs": [
{
"nodeId": "generate-image-1",
"label": "Generate Image",
"type": "image",
"url": "https://…/output.png"
}
]
}
```

전체 흐름을 스크립트 하나로 만든 예시와, 같은 호출을 TypeScript로 작성한 예시입니다.

**curl**

```bash
BASE="https://app.nodaro.ai"
WORKFLOW_ID="8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f"

EXEC=$(curl -s -X POST "$BASE/v1/api/run" \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"workflowId\": \"$WORKFLOW_ID\", \"inputs\": {\"text-prompt-1\": {\"text\": \"a cat at sunset\"}}}" \
| jq -r .executionId)

while true; do
STATUS=$(curl -s -H "Authorization: Bearer $NODARO_API_KEY" \
"$BASE/v1/api/status/$EXEC" | jq -r .status)
echo "Status: $STATUS"
case "$STATUS" in completed|failed|cancelled|timed_out|discarded) break;; esac
sleep 5
done

curl -s -H "Authorization: Bearer $NODARO_API_KEY" "$BASE/v1/api/result/$EXEC" | jq .
```

**TypeScript SDK**

```ts
// The /v1/api/ lane has no dedicated SDK method: call it with client.request.
const schema = await client.request('GET', '/v1/api/schema', {
query: { workflowId },
})

const { executionId } = await client.request<{ executionId: string }>('POST', '/v1/api/run', {
body: { workflowId, inputs: { 'text-prompt-1': { text: 'a cat at sunset' } } },
})

const final = ['completed', 'failed', 'cancelled', 'timed_out', 'discarded']
let status = 'pending'
while (!final.includes(status)) {
await new Promise((r) => setTimeout(r, 3_000))
;({ status } = await client.request<{ status: string }>('GET', `/v1/api/status/${executionId}`))
}

const result = await client.request('GET', `/v1/api/result/${executionId}`)
```

CLI는 워크플로를 저장된 값으로만 실행합니다. 터미널에서 값을 넘기려면 워크플로를 앱으로 게시하고 `nodaro apps run <slug> --input prompt="…"`를 실행하세요.

<TypeTable
type={{
workflowId: { type: 'string (uuid)', description: '실행할 워크플로입니다.', required: true },
inputs: { type: 'object', description: '선택 사항. 입력 노드의 새 값으로, 노드 ID 또는 고유한 노드 레이블을 키로 합니다. 각 값은 그 노드의 필드를 담은 객체입니다. 예: { text: "…" }.' },
}}
/>

워크플로 스코프가 있는 토큰은 그 스코프 안의 워크플로만 실행할 수 있습니다. 다른 워크플로에는 `403 forbidden`이 반환됩니다. `POST /v1/api/run`과 `GET /v1/api/workflows`는 토큰의 분당 한도에 포함되지만, 상태, 결과, 스키마 읽기는 포함되지 않습니다. [요청 한도](https://nodaro.ai/docs/developers/api/rate-limits)를 참고하세요.

## 동기 또는 비동기
`POST /v1/api/run`은 기본적으로 비동기입니다. 즉시 `{ executionId, status: "pending" }`을 담은 `202 Accepted`를 반환하고, 그다음 폴링합니다.

짧은 워크플로는 끝날 때까지 연결을 유지할 수 있습니다.

```http
POST /v1/api/run?wait=true&timeout=120
```

- 서버는 최대 `timeout`초 동안 5초마다 실행을 확인합니다. 기본값은 120이고 최댓값은 600입니다.
- 실행이 시간 안에 끝나면 응답은 `GET /v1/api/result/:execId`와 같은 페이로드입니다. 그 `status`는 `completed`, `failed`, `cancelled`, `timed_out`, `discarded` 중 하나입니다.
- 끝나지 않으면 응답은 `{ executionId, status: "pending" }`을 담은 `202`이며, 그때부터 폴링하세요.

텍스트 생성이나 가벼운 이미지 작업처럼 1분 안에 끝날 것으로 예상되는 실행에는 동기 방식을 사용하세요. 동영상을 렌더링하거나 업스케일하는 워크플로에는 비동기 방식을 사용하세요. SDK에서는 클라이언트가 기본적으로 60초 뒤에 포기하므로, `createClient`의 `timeoutMs`를 `timeout`보다 크게 설정하세요.

`POST /v1/workflows/:id/run`과 생성 라우트는 항상 비동기입니다. 노드 하나는 SDK의 `nodes.runAndWait`가 작업을 대신 폴링합니다. [단일 노드 실행](https://nodaro.ai/docs/developers/api/nodes)을 참고하세요.

## 새 내용이 없는 실행
예약된 실행은 읽는 피드에 새 게시물이 없어서 할 일이 없는 경우가 많습니다. 이런 실행은 **`failed`가 아니라 `completed`로 끝납니다**.

- 모델이나 보이스에 텍스트를 보내는 노드는, 보낼 텍스트가 비어 있고 그 텍스트가 이번 실행에서 아무것도 만들지 않은 노드에서 왔을 때 건너뜁니다. 그 뒤의 노드도 함께 건너뜁니다. 이런 노드는 [텍스트 생성](https://nodaro.ai/docs/nodes/automate/prompt), [대본 생성](https://nodaro.ai/docs/nodes/video/generate-script), [텍스트 음성 변환](https://nodaro.ai/docs/nodes/audio/text-to-speech), [음악 생성](https://nodaro.ai/docs/nodes/audio/generate-music), [텍스트로 오디오 만들기](https://nodaro.ai/docs/nodes/audio/text-to-audio)입니다.
- [Webhook 출력](https://nodaro.ai/docs/nodes/publish/webhook-output)이나 소셜 게시 노드처럼 연결된 것을 보내는 출력 노드는, 이번 실행에서 모든 연결이 아무것도 전달하지 않았을 때 건너뜁니다.
- 건너뛴 노드는 청구되지 않습니다. 새 게시물을 반환하지 않은 [Telegram 채널 피드](https://nodaro.ai/docs/nodes/automate/telegram-channel-feed)도 청구되지 않습니다.

이런 실행에 대해 `GET /v1/api/status/:execId`, `GET /v1/api/result/:execId`, 그리고 `?wait=true` 응답은 `"outcome": "nothing_new"`를 담습니다. 완료된 다른 모든 실행은 `"succeeded"`입니다. 실패하거나 취소된 실행에는 `outcome`이 없고 `status`만 있습니다. 잘못된 것이 없으므로 `errorMessage`는 `null`로 남습니다. `GET /v1/workflow-executions/:id`에서 건너뛴 각 노드는 `{ "status": "skipped", "skipReason": "empty_input" }`입니다.

직접 입력한 프롬프트가 있는 노드는 연결이 비어 있어도 실행됩니다. 그 프롬프트가 요청이기 때문입니다. 연결이 전혀 없는 노드는 여전히 평소의 유효성 검사 오류로 실패합니다. 이번 실행에서 아무것도 전달하지 않은 연결만 해당됩니다.

## 실행에서 바꿀 수 없는 것
실행 요청으로 워크플로의 외부 전송 노드가 가리키는 곳을 바꿀 수 없습니다. 워크플로가 어디로 보내고 어디에서 가져오는지는 `POST /v1/workflows/:id/run`, `POST /v1/api/run`, `POST /v1/app/:slug/run`을 포함한 모든 실행 엔드포인트에서 워크플로 자체가 정합니다.

외부 전송 노드는 [Webhook 출력](https://nodaro.ai/docs/nodes/publish/webhook-output), [SNS에 게시](https://nodaro.ai/docs/nodes/publish/publish-to-social) 같은 SNS 게시 노드, 그리고 [웹 스크래핑](https://nodaro.ai/docs/nodes/automate/web-scrape), [Telegram 채널 피드](https://nodaro.ai/docs/nodes/automate/telegram-channel-feed), [동영상 URL](https://nodaro.ai/docs/nodes/automate/video-url) 같은 가져오기 노드입니다. 이런 노드에서 재정의로 바꿀 수 없는 항목은 다음과 같습니다.

- 이름이 `Url` 또는 `Urls`로 끝나는 모든 필드
- `target`, `targets`, `query`, `channel`, `chatId`, `connectionId`, `credentialId`, `platform`, `webhook`, `endpoint`, `host`, `privacy`
- 가져오기 노드가 어느 대상 필드를 읽을지 정하는 `actor`와 `mode` 선택자

이 규칙은 객체나 `fieldMappings` 항목 안에 중첩된 값에도 적용되며, 새 주소뿐 아니라 빈 값에도 적용됩니다. 대상을 비우면 그 노드가 대신 상위 노드의 텍스트를 읽게 되기 때문입니다. 이런 요청은 아무것도 실행되기 전에 `400 locked_field`를 반환합니다. 오류에는 문제가 된 필드가 최대 10개까지 이름으로 나오고 나머지는 개수로 표시됩니다. 이런 노드에서 32단계보다 깊이 중첩된 재정의는 바로 거부됩니다.

이런 노드의 캡션이나 한도 같은 일반 필드, 그리고 업로드나 레퍼런스 오디오 같은 입력 노드의 미디어 `url` 필드는 계속 재정의할 수 있습니다.

실행은 [하위 워크플로](https://nodaro.ai/docs/nodes/automate/sub-workflow) 노드가 실행하는 대상도 바꿀 수 없습니다. 이 노드의 `workflowId`, `selectedRouteId`, `routeSnapshot`은 직접 설정하든 `fieldMappings` 항목으로 설정하든 `400 locked_field`로 응답합니다. 노드가 어떤 워크플로를, 그중 어느 부분을 실행할지는 워크플로 자체가 정합니다.

UGC 동영상 노드에서 실행이 설정할 수 있는 것은 UGC 크리에이터의 `source`, `gender`, `photoUrl`과 UGC 스크립트의 `targetDurationSec`뿐입니다. 유지된 결과, 스크립트, 클립 같은 다른 필드는 `400 locked_field`로 응답합니다. **내 사진 사용**으로 설정된 UGC 크리에이터를 실행하면서 실행하는 사람 본인의 사진을 주지 않으면, 아무것도 청구되기 전에 `400 ugc_photo_required`로 응답합니다. [UGC 크리에이터](https://nodaro.ai/docs/nodes/video/ugc-creator)와 [앱의 UGC 동영상](https://nodaro.ai/docs/concepts/apps#a-ugc-video-in-an-app)을 참고하세요.

게시된 앱에서, 선택지가 정해진 목록인 노출된 모델이나 빠른 설정은 그 목록으로 제한됩니다. 목록 밖의 값은 `400 validation_error`로 응답하며, 설정의 범위를 벗어난 숫자도 마찬가지입니다. `inputOverrides`의 `null`은 그 실행에서 노드의 해당 필드를 비우므로 노드 자체의 기본값이 적용됩니다. 반면 플랫 입력의 `null`은 버려지므로 저장된 값이 적용됩니다. [모델 및 빠른 설정 필드](https://nodaro.ai/docs/developers/embed/miniapps#quick-settings)를 참고하세요.

## 매개변수 보정
이미지와 동영상 생성 라우트는 모델과 관계없이 `aspectRatio`, `resolution`, `quality`에 하나의 공통 값 체계를 받습니다. 그 전부를 지원하는 모델은 없습니다. 서버는 선택한 모델이 받지 않는 값을 거부하는 대신 모델이 받는 값으로 **보정**하고, 무엇을 바꿨는지 알려 줍니다. 워크플로 도중에 거부하면 이미 생성되어 비용이 청구된 옆 노드까지 모두 실패하기 때문입니다.

값을 보정하는 라우트는 `POST /v1/generate-image`, `/v1/image-to-image`, `/v1/edit-image`, `/v1/text-to-video`, `/v1/generate-video`입니다.

```json
{
"jobId": "0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10",
"adjustments": [
{
"field": "aspectRatio",
"from": "3:2",
"to": "auto",
"reason": "GPT Image 2 does not support aspectRatio \"3:2\" — using \"auto\" instead. Supported: auto, 1:1, 16:9, 9:16, 4:3, 3:4."
},
{
"field": "resolution",
"from": "4K",
"to": "1K",
"reason": "GPT Image 2 only renders 1K at the \"auto\" aspect ratio."
}
]
}
```

- **바뀐 것이 없으면 `adjustments`가 없습니다.** 유효한 요청의 응답은 정확히 `{ "jobId": "…" }`입니다.
- **모델에 그런 설정이 없으면 `to`가 없고** 값은 버려집니다. 예를 들어 업스케일러에 보낸 `aspectRatio`가 그렇습니다.
- **크레딧은 보정된 값을 따릅니다.** [GPT Image 2](https://nodaro.ai/docs/models/image/gpt-image-2)에 `2K`의 `auto`를 요청하면 1K로 렌더링되고 1K로 청구됩니다. `auto`는 1K로만 렌더링되기 때문입니다.
- **`aspectRatio: "auto"`는 사진의 모양을 유지합니다.** `/v1/image-to-image`와 `/v1/edit-image`에 해당합니다. 자체 `auto`가 있는 모델은 그대로 받습니다. 없는 모델에서는 서버가 `imageUrl`의 크기를 읽어 모델이 지원하는 가장 가까운 비율을 쓰고, 그 선택을 `adjustments`에 보고합니다. 예를 들어 `seedream-5-pro-i2i`에 보낸 1104x1472 세로 이미지는 `3:4`로 렌더링됩니다. 크기를 읽을 수 없으면 `auto`는 모델 목록의 첫 비율로 대체됩니다. 이 모델들에서는 비율이 가격을 바꾸지 않습니다.
- **`/v1/generate-image`도 사진이 연결되면 똑같이 동작합니다.** 사진은 요청이 모델에 보내는 첫 번째 이미지입니다. 설정되어 있으면 `baseImageUrl`(인페인트 또는 리파인), 아니면 첫 번째 레퍼런스 이미지(`referenceImageUrls`, 또는 `connectedReferences`로 만든 첫 번째 이미지)입니다. 사진이 없으면, 자체 `auto`가 없는 모델에서 `auto`는 모델 목록의 첫 비율이 됩니다.
- **저장된 워크플로도 보정됩니다.** API나 MCP로 저장한 워크플로는 저장될 때 같은 보정을 거칩니다. 예외는 이미지 생성, 이미지 투 이미지, 이미지 수정, 이미지 편집 노드의 `auto`입니다. 이 값은 그대로 유지되고, 워크플로가 실행될 때 사진을 기준으로 정해집니다.
- **모델마다 받는 값**은 `GET /v1/models`에 있습니다. [모델 찾기](https://nodaro.ai/docs/developers/api/nodes#discover-models)를 참고하세요.

### 동영상 라우트의 보정
`/v1/text-to-video`와 `/v1/generate-video`는 같은 형태로 `adjustments`를 반환합니다. `/v1/generate-video`는 각 보정의 `reason`을 `warnings` 배열에도 한 번 더 담으며, 이 배열에는 `voice_unsupported_for_provider`처럼 매개변수와 관계없는 경고도 함께 들어갑니다. 어떤 설정이 바뀌었는지 알아야 할 때는 `adjustments`를 읽으세요. 두 값이 서로 다른 경우는 없습니다.

- **지원하지 않는 값은 가장 가까운 선택지로 맞춰집니다.** 가장 저렴한 값이나 첫 번째 값으로 바뀌지 않습니다. 1080p까지만 지원하는 모델에 `4k`를 요청하면 480p가 아니라 1080p로 렌더링되고, 세로형 `9:21`은 `16:9`가 아니라 `9:16`이 됩니다.
- **`resolution`을 생략하면 가격이 매겨진 구간으로 보냅니다.** 플랫폼이 모델의 기본 구간을 정해 둔 경우, 그 구간으로 청구되고 모델에도 그 구간을 요청합니다. 이 값은 작업의 `input_data`에 나타나므로, 무엇을 보냈는지 언제든 확인할 수 있습니다.
- **가격을 매기기 전에 표기를 정규화합니다.** `4K`는 `4k`로 읽히므로 4K 구간으로 가격이 매겨지고 렌더링됩니다.
- **일부 모델은 다른 값에 대해 고정된 구간으로 렌더링합니다.** [MiniMax H3](https://nodaro.ai/docs/models/video/minimax-h3)은 `768P`가 아닌 모든 값을 2K로 렌더링하고, [Wan 3.0](https://nodaro.ai/docs/models/video/wan-3-0) 제품군은 720p로 렌더링합니다. 이런 모델은 실제로 만들어질 구간으로 보정되므로 가격이 렌더링 결과와 일치합니다. `GET /v1/models`의 모델 항목은 이를 `unlistedResolutionRendersAs`에 밝힙니다.
- **길이는 모델이 렌더링하는 길이로 청구됩니다.** `duration`이 없는 요청은 모델의 기본 길이, 즉 `GET /v1/models`의 `defaultDuration`으로 렌더링되고 그만큼 청구됩니다. 모델이 제공하지 않는 `duration`은 제공하는 가장 가까운 길이로 옮겨지며, 같은 거리라면 짧은 쪽으로 옮겨집니다. 이 변경은 `adjustments`에 보고됩니다. 예를 들어 7초 요청은 Kling에서 5초로, [LTX 2.3](https://nodaro.ai/docs/models/video/ltx-2-3-pro) 모델에서 6초로 렌더링되고 청구됩니다.
- **`duration: -1`은 지원하는 모델에서 자동을 뜻합니다.** [Seedance 2](https://nodaro.ai/docs/models/video/seedance-2) 제품군(`GET /v1/models`에서 `autoDuration: true`)이 이를 지원합니다. 모델이 클립 길이를 고르며, 레퍼런스 동영상을 편집할 때는 원본 클립의 길이가 됩니다. 자동 실행은 모델의 가장 긴 클립만큼 크레딧을 예약하고, 실제로 만들어진 길이에 맞춰 차액을 환불합니다. 다른 모델은 `-1`을 무시하고 기본 길이로 렌더링합니다.

캐릭터와 장소 이미지 라우트(`/v1/generate-character`, `/v1/generate-character-asset`, `/v1/generate-location`, `/v1/generate-location-asset`)도 같은 방식으로 `quality`와 `resolution`을 보정하지만 `adjustments`를 반환하지는 않습니다. 보정된 값은 `GET /v1/jobs/:id`로 가져온 작업의 `input_data`에서만 볼 수 있습니다.

## 워크플로 관리
### 목록과 읽기
`GET /v1/projects/:projectId/workflows`는 프로젝트의 워크플로를 `nodes`, `edges`, `settings` 없이 반환합니다. `GET /v1/workflows/:id`는 워크플로 하나를 전부 반환합니다. 조직에서는 목록이 내가 작업하는 워크스페이스를 따릅니다. [워크스페이스](https://nodaro.ai/docs/developers/api/workspaces)를 참고하세요.

**curl**

```bash
curl -s https://app.nodaro.ai/v1/projects/$PROJECT_ID/workflows \
  -H "Authorization: Bearer $NODARO_API_KEY" | jq '.data[] | {id, name}'
```

**TypeScript SDK**

```ts
const { data: workflows } = await client.workflows.list({ projectId })
const { data: workflow } = await client.workflows.get(workflows[0].id)
console.log(workflow.nodes.length)
```

**CLI**

```bash
nodaro workflows list --project $PROJECT_ID --json
nodaro workflows get 8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f
```

### 만들기와 수정
`POST /v1/projects/:projectId/workflows`로 프로젝트에 워크플로를 만드세요. 프로젝트를 제외한 모든 값은 선택 사항이며, 생략하면 서버의 기본값이 쓰입니다.

```ts
const { data: wf } = await client.workflows.create({
projectId,
name: 'My workflow',
nodes: [],
edges: [],
})
```

`PATCH /v1/workflows/:id`는 원하는 필드만 골라 변경합니다. 동시 편집에 안전하게 수정하려면 마지막으로 읽었을 때의 정수 `version`을 `expectedVersion`으로 보내세요. 그 뒤에 워크플로가 바뀌었다면 수정은 `409 workflow_conflict`로 거부되고, 오류에는 `currentVersion`, `currentUpdatedAt`, 그리고 현재 워크플로 전체인 `currentRecord`가 담깁니다. 다시 읽지 않고 `currentRecord`에 변경 사항을 병합한 뒤 다시 저장하세요. 타임스탬프인 `expectedUpdatedAt`도 같은 방식으로 작동합니다.

```ts

try {
await client.workflows.update(id, { name: 'Renamed', expectedVersion: 7 })
} catch (err) {
if (err instanceof WorkflowConflictError && err.currentRecord) {
await client.workflows.update(id, { name: 'Renamed', expectedVersion: err.currentVersion })
} else throw err
}
```

- 노드의 `data`에 있는 실행 상태 값, 예를 들어 실행 상태, 현재 작업, 진행률은 저장할 때 제거되며 저장되지 않습니다.
- `thumbnailUrl`은 이미 호스팅된 이미지로 워크플로의 미리 보기 이미지를 설정합니다. `null`을 보내면 지워집니다.
- `edges`만 보내면서 [동영상 오버레이](https://nodaro.ai/docs/nodes/video/video-overlay) 레이어를 연결하는 저장은 그 노드를 다시 쓰므로, 읽은 뒤 워크플로가 바뀌었다면 `expectedVersion`이 없어도 `409 workflow_conflict`를 반환합니다.
- `visibility`는 `private` 또는 `workspace`입니다. 만든 사람이나 워크스페이스 관리자만 바꿀 수 있으며, 워크스페이스 밖의 워크플로에는 `400 not_workspace_scoped`가 반환됩니다.

### 삭제
`DELETE /v1/workflows/:id`는 워크플로를 삭제합니다. 만든 사람과 워크스페이스 관리자는 삭제할 수 있지만, 공동 작업자는 삭제할 수 없습니다. 존재하지 않거나 내가 볼 수 없는 워크플로를 삭제하면 `404`가 반환되므로, 삭제가 조용히 성공한 것처럼 보이는 일은 없습니다. 워크플로를 볼 수는 있지만 삭제할 수 없는 공동 작업자에게는 `403`이 반환됩니다.

### 내보내기와 가져오기
`GET /v1/workflows/:id/export`는 워크플로를 다른 곳으로 옮길 수 있는 JSON 번들로 반환합니다. `?assets=true`를 붙이면 워크플로가 쓰는 캐릭터, 오브젝트, 장소가 내 것일 경우 번들에 함께 담깁니다. `localhost`나 사설 네트워크의 파일처럼 다른 설치 환경에서 가져올 수 없는 미디어를 노드가 가리키면, 번들은 그 미디어를 `portability.unreachableMedia`에 나열합니다. 보기 권한만 있는 사람은 소유자의 Studio 초안을 뺀 워크플로를 받습니다. 빈 미디어 슬롯, 진행 중인 실행 표시, 테이크의 보이스 계획입니다. 이때 번들에는 그 부분만 쓰는 에셋이 담기지 않습니다. `GET /v1/workflows/:id`도 보기 권한만 있는 사람에게는 같은 부분을 빼고 반환합니다.

`POST /v1/workflows/import`는 번들로 워크플로를 만듭니다.

```http
POST /v1/workflows/import
{ "projectId": "<project uuid>", "workflow_json": { "version": 1, "name": "…", "nodes": [], "edges": [] } }
```

가져오기는 번들에 담긴 캐릭터, 오브젝트, 크리처, 장소를 내 계정 아래에 다시 만들고, 노드가 그것을 가리키도록 바꿉니다. 가져올 수 있는 미디어는 이 설치 환경의 저장소로 복사합니다. 워크플로의 미디어는 최대 25개 파일, 번들에 담긴 엔티티는 추가로 최대 25개 파일이며, 이미지는 최대 20MB, 동영상과 오디오는 최대 50MB입니다. 응답에는 새 워크플로와 `importReport`가 담깁니다.

| 필드 | 의미 |
| --- | --- |
| `rehosted` | 이 설치 환경으로 복사된 파일 수입니다. |
| `unreachable` | 사설 호스트에 있어 원래 위치를 그대로 가리키는 미디어입니다. 파일을 다시 업로드할 때까지 해당 노드는 실행되지 않습니다. |
| `skipped` | 복사하지 못한 미디어와 그 이유입니다. 예: `HTTP 404`. |
| `assetIdMap` | 번들에 담긴 각 엔티티 ID와, 그 엔티티를 위해 만든 행의 대응표입니다. |
| `assetsSkipped` | 저장 용량이 부족해 만들지 못한 엔티티입니다. 워크플로 자체는 만들어집니다. |

CLI에서는 `nodaro workflows export <id> --with-assets --output bundle.json`을 실행한 다음 `nodaro workflows import bundle.json --project <projectId>`를 실행하세요. 무엇이 함께 옮겨지는지는 [가져오기와 내보내기](https://nodaro.ai/docs/guides/import-export)에서 확인하세요.

### 다른 프로젝트로 이동
```http
POST /v1/workflows/:id/move
{ "projectId": "…" }
```

이동은 워크플로 쓰기이므로 OAuth 토큰에는 `workflows:write`가 필요합니다. `projectId`를 담은 `PATCH /v1/workflows/:id`도 같은 일을 하며 같은 규칙을 따릅니다. 내가 만든 작업은 옮길 수 있습니다. 조직 안에서는 워크스페이스 관리자가 자신이 관리하는 두 워크스페이스 사이에서 작업을 옮길 수도 있습니다. 개인 프로젝트는 옮기기 전과 후 모두 내 것이어야 합니다.

| 상태 | 코드 | 의미 |
| --- | --- | --- |
| 400 | `validation_error` | 워크플로가 이미 그 프로젝트에 있습니다. |
| 403 | `not_permitted` | 내가 옮길 수 있는 워크플로가 아니거나, 그곳으로 옮길 수 없습니다. |
| 404 | `not_found` | 그런 워크플로가 없거나, 나에게 그런 프로젝트가 없습니다. |
| 409 | `move_blocked` | 과제를 위해 만든 작업입니다. |
| 409 | `workspace_archived` | 대상 워크스페이스가 보관되어 있습니다. |

워크스페이스가 바뀌는 이동은 워크플로의 공동 작업자 권한을 제거하고 그 내용을 보고하므로, 액세스를 잃은 사람에게 알릴 수 있습니다.

```json
{
"data": { "id": "8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f", "projectId": "5e4d3c2b-1a09-4f8e-8d7c-6b5a49382716" },
"droppedCollaborators": [{ "userId": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f70812", "name": "Sam" }]
}
```

`PATCH` 방식은 권한이 제거된 경우에만 `droppedCollaborators`를 포함합니다.

두 방식 모두 옮겨진 워크플로를 현재 상태 그대로 `data`에 담아 반환합니다. 이동 후 내 접근 권한이 `edit`보다 낮으면, `view` 권한 사용자가 받는 형태로 받습니다. 예를 들어 워크스페이스 관리자가 멤버의 작업을, 관리자가 보기만 할 수 있는 워크스페이스로 옮길 때 그렇습니다. [워크스페이스와 조직](https://nodaro.ai/docs/developers/api/workspaces)을 참조하세요.

## 워크플로의 OAuth 스코프
| 스코프 | 라우트 |
| --- | --- |
| `workflows:read` | `GET /v1/projects/:projectId/workflows`, `GET /v1/workflows`, `GET /v1/workflows/:id`, `GET /v1/workflows/:id/export` |
| `workflows:write` | 만들기, 수정, 삭제, 가져오기, 이동, 그리고 `POST /v1/workflows/:parentId/sub-workflows` |
| `workflows:execute` | `POST /v1/workflows/:id/run` |

개인 API 토큰에는 스코프가 필요하지 않습니다. [OAuth 앱](https://nodaro.ai/docs/developers/oauth)을 참고하세요.

## 스튜디오 프로덕션 노드
캔버스의 일부 노드는 스튜디오 프로덕션에 속하며 연결된 프레임에 의존합니다. 이런 노드는 스튜디오 프로덕션 API로 생성해야 합니다. `POST /v1/workflows/:id/run`으로 실행하거나 직접 생성하면 `400 sequence_execution_required`가 반환됩니다. [스튜디오 프로덕션](https://nodaro.ai/docs/developers/api/studio-productions)을 참고하세요.

## 자주 묻는 질문

### API로 Nodaro 워크플로를 실행하려면 어떻게 하나요?

POST /v1/workflows/:id/run을 보내세요. 저장된 워크플로가 실행되고, 폴링할 executionId와 함께 202가 반환됩니다. 한 번의 실행에서만 입력값을 바꾸려면 노드 ID를 키로 하는 inputOverrides 객체를 추가하거나, inputs 객체와 함께 POST /v1/api/run을 사용하세요.

### 워크플로 실행에 입력값을 전달하려면 어떻게 하나요?

POST /v1/api/run에 inputs 객체를 보내세요. 키는 노드 ID 또는 고유한 노드 레이블이고, 그 안에 text 같은 노드의 필드를 넣습니다. GET /v1/api/schema는 워크플로의 입력 필드 목록을 반환합니다.

### 요청 한 번으로 워크플로 결과를 기다릴 수 있나요?

네. POST /v1/api/run에 ?wait=true&timeout=120을 추가하세요. 서버가 최대 600초 동안 연결을 유지하고 출력을 반환합니다. 실행이 아직 진행 중이면 executionId와 함께 202를 반환하므로, 그때부터 폴링하세요.

### 보낸 화면 비율이나 해상도를 API가 바꾼 이유는 무엇인가요?

선택한 모델이 그 값을 지원하지 않아서, Nodaro가 실행을 실패시키는 대신 모델이 받는 가장 가까운 값으로 보정했기 때문입니다. 응답의 adjustments에 모든 변경 사항이 나열되며, 크레딧은 보정된 값을 기준으로 청구됩니다.

### API 실행으로 워크플로가 결과를 보내는 곳을 바꿀 수 있나요?

아니요. Webhook 출력의 URL이나 SNS 게시 노드의 계정처럼 외부로 보내는 노드의 대상 필드는 실행에서 재정의할 수 없습니다. 재정의를 시도한 요청에는 400 locked_field가 반환됩니다.
