# תהליכים ופרויקטים

> יצירה, עדכון, שיתוף, ייצוא, ייבוא והרצה של תהליכי Nodaro מ-TypeScript עם client.workflows, וארגון שלהם בפרויקטים עם client.projects.

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

**`client.workflows`** קורא, כותב, משתף ומריץ תהליכי Nodaro, והמשאב **`client.projects`** מנהל את הפרויקטים שבהם נמצאים התהליכים. המשאב **`client.sessions`** מקבץ יצירות בודדות לסשנים בתוך פרויקט. תהליך הוא קנבס של רכיבים מחוברים, וכל תהליך שייך לפרויקט אחד. המתודות קוראות לנקודות הקצה שבדף ה-API [תהליכים](https://nodaro.ai/docs/developers/api/workflows). ההסבר על המושגים עצמם נמצא בדף [תהליכים ופרויקטים](https://nodaro.ai/docs/concepts/workflows-and-projects).

## מתודות
| מתודה | מה היא עושה |
| --- | --- |
| [`workflows.list(params)`](#listparams) | רשימת התהליכים של פרויקט, בלי הגרפים שלהם |
| [`workflows.get(id)`](#getid) | קריאה של תהליך אחד, עם הרכיבים, החיבורים וההגדרות שלו |
| [`workflows.getPublic(id)`](#getpublicid) | קריאה של תהליך ששותף בקישור, בלי אסימון |
| [`workflows.create(input)`](#createinput) | יצירת תהליך בפרויקט |
| [`workflows.update(id, input)`](#updateid-input) | שינוי של שדות כלשהם בתהליך |
| [`workflows.delete(id)`](#deleteid) | מחיקת תהליך |
| [`workflows.run(id, params?)`](#runid-params) | התחלת הרצה של התהליך |
| [`workflows.renderFinal(id, params)`](#renderfinalid-params) | רינדור הגרסה הסופית של רינדור ברכיב החלת רשימת עריכה, אחרי בדיקת התצוגה המקדימה שלו |
| [`workflows.estimateRenderFinal(id, params)`](#estimaterenderfinalid-params) | אומדן מחיר לאותו רינדור סופי, לפני שמתחילים אותו |
| [`workflows.export(workflowId, opts?)`](#exportworkflowid-opts) | ייצוא תהליך כחבילת JSON ניידת |
| [`workflows.import(input)`](#importinput) | ייבוא חבילה לפרויקט |
| [`workflows.setVisibility(id, visibility)`](#setvisibilityid-visibility) | הגדרת תהליך כפרטי או כגלוי לסביבת העבודה שלו |
| [`workflows.move(id, params)`](#moveid-params) | העברת תהליך לפרויקט אחר |
| [`workflows.sharedWithMe()`](#sharedwithme) | רשימת התהליכים שאנשים אחרים שיתפו איתכם |
| [`workflows.listMcpConversations(params?)`](#listmcpconversationsparams) | רשימת שיחות ה-MCP שלכם בחודש אחד |
| [`workflows.collaborators.*`](#collaborators) | רשימה, הוספה, שינוי והסרה של האנשים שהתהליך משותף איתם |
| [`projects.list()`](#projectslist) ועוד | רשימה, קריאה, יצירה, עדכון ומחיקה של פרויקטים |
| [`sessions.create(input)`](#clientsessions) ועוד | סשני יצירה בתוך פרויקט, והמשימות שנמצאות בהם |

## client.workflows
### list(params)‎
מחזירה את רשימת התהליכים בפרויקט. הרשימה כוללת רק מטא־דאטה: השדות `nodes`, `edges`, `settings` ו-`sourcePrompt` מושמטים. כדי לקבל את הגרף המלא של תהליך, קראו ל-`get()`.

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

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "הפרויקט שאת התהליכים שלו רוצים לקבל." },
}}
/>

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

כשהפרויקט לא גלוי לכם, המתודה זורקת `NotFoundError`.

### get(id)‎
מחזירה תהליך אחד, כולל ה-`nodes`, ה-`edges` וה-`settings` המלאים שלו.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה התהליך." },
}}
/>

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

שמרו את `version` אם אתם מתכננים לעדכן את התהליך: הערך הזה מגן על העדכון מפני שינויים במקביל של מישהו אחר.

### getPublic(id)‎
מחזירה תהליך שהבעלים שלו שיתף בקישור (`GET /v1/public/workflows/:id`). לא צריך אסימון. התהליך חוזר רק כל עוד השיתוף שלו מופעל; אחרת המתודה זורקת `NotFoundError`. תהליך של סרטון UGC חוזר לכל אחד חוץ מהבעלים באותה צורה שבה אפליקציה שפורסמה מספקת אותו: רכיבי ה-UGC שלו, וכל רכיב שנוצר מהם, בלי התוצאות שלהם, ואף פעם לא עם התמונה של הבעלים.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "המזהה של התהליך המשותף." },
}}
/>

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

כשספק האימות לא מחזיר אסימון, הבקשה נשלחת בלעדיו.

הפקת Studio משותפת חוזרת בלי מצב העבודה של הבעלים שלה. הפח, ההרצות שעדיין רצות, משבצות המדיה הריקות וטיוטת העורך שלא נשמרה מושמטים מ-`settings.studio`.

### create(input)‎
יוצרת תהליך בפרויקט ומחזירה את הרשומה המלאה. נדרשים רק `projectId` ו-`name`; שאר השדות מקבלים את ברירות המחדל של השרת.

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

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "הפרויקט שבו התהליך נוצר." },
name: { type: 'string', required: true, description: "שם התהליך." },
description: { type: 'string', description: "תיאור." },
folderId: { type: 'string | null', description: "התיקייה בתוך הפרויקט." },
nodes: { type: 'GenericNode[]', description: "הרכיבים על הקנבס." },
edges: { type: 'GenericEdge[]', description: "החיבורים בין הרכיבים." },
settings: { type: 'Record<string, unknown>', description: "הגדרות התהליך." },
sourcePrompt: { type: 'string', description: "הפרומפט שממנו התהליך נוצר, אם יש כזה." },
}}
/>

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

כדי לקבל גרף מוכן, ייצאו תהליך שבניתם בעורך והשתמשו ב-`nodes` וב-`edges` שלו, או שכפלו [תבנית](https://nodaro.ai/docs/developers/sdk/apps-and-templates).

### update(id, input)‎
משנה כל צירוף של שדות בתהליך ומחזירה את הרשומה המעודכנת המלאה (`PATCH /v1/workflows/:id`). שדות שלא שולחים נשארים כמו שהם.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה התהליך." },
name: { type: 'string', description: "שם חדש." },
description: { type: 'string', description: "תיאור חדש." },
folderId: { type: 'string | null', description: "מעביר את התהליך לתיקייה אחרת באותו פרויקט." },
nodes: { type: 'GenericNode[]', description: "הרשימה החדשה והמלאה של הרכיבים." },
edges: { type: 'GenericEdge[]', description: "הרשימה החדשה והמלאה של החיבורים." },
settings: { type: 'Record<string, unknown>', description: "הגדרות חדשות לתהליך." },
sourcePrompt: { type: 'string', description: "הפרומפט שממנו התהליך נוצר." },
thumbnailUrl: { type: 'string | null', description: "תמונת השער: הכתובת של תמונה שכבר מאוחסנת, או null כדי להסיר אותה." },
expectedVersion: { type: 'number', description: "הגרסה שקראתם. כשהגרסה השמורה שונה, העדכון נדחה עם WorkflowConflictError. עדיף על expectedUpdatedAt." },
expectedUpdatedAt: { type: 'string', description: "הערך של updatedAt שקראתם, כחלופה ישנה יותר ל-expectedVersion." },
}}
/>

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

**עדכונים בטוחים.** בלי `expectedVersion` או `expectedUpdatedAt`, הכתיבה האחרונה גוברת. עם אחד מהם, העדכון חל רק אם אף אחד לא שינה את התהליך מאז שקראתם אותו. אחרת המתודה זורקת [`WorkflowConflictError`](https://nodaro.ai/docs/developers/sdk/errors#workflowconflicterror), וב-`currentRecord` שלה נמצא התהליך הנוכחי, כך שאפשר למזג בלי קריאה נוספת:

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

השרת מסיר מהנתונים של הרכיבים ערכים של מצב ההרצה, כגון סטטוס ההרצה או המזהה של המשימה הנוכחית, והם אף פעם לא נשמרים.

### delete(id)‎
מוחקת תהליך.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה התהליך." },
}}
/>

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

כשהמזהה לא קיים או לא שייך לכם, המתודה זורקת `NotFoundError`, כך שמחיקה אף פעם לא נכשלת בשקט.

### run(id, params?)‎
מתחילה הרצה של התהליך ומחזירה מיד את `{ executionId, status }`. ההרצה ממשיכה בשרת. כדי לקבל את ההתקדמות והתוצאה שלה, בדקו את הסטטוס מדי כמה שניות עם [`client.executions.get()`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions).

```ts
run(id: string, params?: {
nodeIds?: string[]
inputOverrides?: Record<string, Record<string, unknown>>
continueFromExecutionId?: string
continueMode?: "from-failed"
}, options?: { idempotencyKey?: string }): Promise<{ executionId: string; status: "pending" | "running" }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה התהליך." },
nodeIds: { type: 'string[]', description: "מריץ רק את הרכיבים האלה. השמיטו אותו כדי להריץ את כל התהליך." },
inputOverrides: { type: 'Record<string, Record<string, unknown>>', description: "הגדרות של רכיבים להרצה הזו בלבד, לפי מזהה רכיב, כגון ‎{ n1: { prompt: '...' } }‎. השדות של כל רכיב ממוזגים מעל ההגדרות השמורות שלו, ורכיב שקיבל ערכים כאלה לא משתמש שוב בתוצאות השמורות שלו. שינוי של יעד ברכיב פלט נדחה עם 400 locked_field." },
continueFromExecutionId: { type: 'string', description: "ממשיך הרצה קודמת במקום להתחיל מחדש. ההרצה הקודמת חייבת להיות הרצה שלכם של התהליך הזה שהושלמה, ויחד עם הפרמטר הזה נדרש גם nodeIds. עם continueMode, היא צריכה להיות הרצה שלכם שנכשלה." },
continueMode: { type: '"from-failed"', description: "ממשיך הרצה שנכשלה מהנקודה שבה נעצרה. הפרמטר nodeIds הופך לאופציונלי. אי אפשר לשלב אותו עם renderFinal." },
"options.idempotencyKey": { type: 'string', description: "נשלח בכותרת Idempotency-Key (לפחות 8 תווים). ניסיון חוזר עם אותו מפתח מקבל בתשובה את ההרצה שכבר התחילה, עם deduped: true." },
}}
/>

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

- כשהחשבון לא יכול לכסות את העלות הגבוהה ביותר האפשרית של ההרצה, המתודה זורקת `InsufficientCreditsError`.
- כל עוד הרצה של התהליך הזה עדיין בעיצומה, קריאה חדשה נכשלת עם [`AlreadyRunningError`](https://nodaro.ai/docs/developers/sdk/errors#alreadyrunningerror) (`409 already_running`). השדה `executionId` שבשגיאה מציין את ההרצה שאת המצב שלה כדאי לבדוק.
- אסימון OAuth צריך את ההרשאה `workflows:execute`.
- כדי להריץ תהליך בסביבת עבודה, קראו ל-`run` בלקוח שמתקבל מ-[`client.withWorkspace()`](https://nodaro.ai/docs/developers/sdk/client#withworkspaceworkspaceid).

**המשך הרצה קודמת.** עם `continueFromExecutionId`, רצים רק הרכיבים שב-`nodeIds`. כל רכיב אחר מעביר הלאה את מה שההרצה הקודמת הפיקה, אף פעם לא את התוצאות השמורות של התהליך. רכיב תוכנית עריכה מעביר הלאה את התוכנית שלו, כולל השינויים שעשיתם בבדיקה, כפי שהם עכשיו. הדריסות שב-`inputOverrides` של ההרצה הקודמת מוחלות שוב, ואלה שאתם שולחים עכשיו מוחלות מעליהן, שדה אחר שדה.

```ts
await client.workflows.run(workflowId, {
nodeIds: [renderNodeId, captionsNodeId],
inputOverrides: { [renderNodeId]: { quality: "final" } },
continueFromExecutionId: previewRunId,
})
```

כשהרצת המשך נדחית, המתודה זורקת שגיאה עוד לפני שנוצרת הרצה כלשהי. כשההרצה הקודמת לא נמצאת, השגיאה היא `NotFoundError` עם הקוד `continuation_not_found`. אחרת, השגיאה היא `NodaroError` שה-`code` שלה הוא `continuation_subset_required` (בלי `nodeIds`), `continuation_workflow_mismatch`, `continuation_version_mismatch` או `continuation_not_completed`.

**המשך הרצה שנכשלה.** עם `continueMode: "from-failed"`, הפרמטר `continueFromExecutionId` הוא הרצה שלכם של התהליך שנכשלה, וההרצה ממשיכה מהנקודה שבה נעצרה. רצים רק הרכיבים שההרצה ההיא לא השלימה: מה שהיא השלימה מועבר הלאה, ולא רץ שוב ולא משלמים עליו שוב. רכיב שנכשל רץ שוב במלואו. הפרמטר `nodeIds` הוא אופציונלי: בלעדיו, ההרצה ממשיכה על הרכיבים שההרצה שנכשלה התחילה עליהם, ובנוסף על כל רכיב שנשאר במצב כשל או שעוכב. המתודה `client.executions.get(id)` מציינת ב-`nodeScope` את הרכיבים שההרצה הראשונה התחילה עליהם (`null` פירושו כל הרכיבים). כל מזהה ב-`nodeIds` נחשב לרכיב שנבחר במפורש: רכיב שמבצע עבודה ושאף הרצה בשרשרת ההרצות לא הריצה אף פעם רץ ונכלל במחיר. רכיב שרץ בהרצה כלשהי בשרשרת לא רץ שוב רק מפני שציינתם אותו. רכיבי מקור והעלאה, טריגרים, בוררים וקונטיינרים מסוג קבוצה או איסוף מעבירים הלאה את מה שההרצה העבירה, ורכיב שהוגדר לדילוג אף פעם לא רץ. שלחו `idempotencyKey`, כדי שניסיון חוזר של אותה קריאה לא יתחיל שום דבר חדש.

```ts
await client.workflows.run(
workflowId,
{ continueFromExecutionId: failedRunId, continueMode: "from-failed" },
{ idempotencyKey: crypto.randomUUID() }, // reuse the same key when you retry this call
)
```

- כשמנסים להמשיך הרצה שהרצת המשך חדשה יותר החליפה, המתודה זורקת `NodaroError` עם הקוד `continuation_superseded`. ההודעה שלה מציינת את הרצת ההמשך הזו: המשיכו אותה במקומה, או מצאו שם את התוצאות שלה, אם היא השלימה הכול.
- כשהרצת ההמשך החדשה ביותר נעצרה בבדיקה של תצוגה מקדימה, המתודה זורקת `ContinuationPreviewPendingError` (הקוד `continuation_preview_pending`, מיוצאת מ-`@nodaro/sdk/errors`). השדה `next` שלה הוא ההרצה שמשלימה את הבדיקה: קראו קודם ל-`client.workflows.run(workflowId, error.next)`, ואז המשיכו.
- כשמנסים להמשיך הרצה שלא נכשלה, המתודה זורקת `continuation_not_failed`. כשמנסים להמשיך הרצה שלא נשאר בה מה להריץ, היא זורקת `continuation_subset_required`.
- כשהקרדיטים שלכם לא מכסים הרצת המשך, המתודה זורקת `InsufficientCreditsError` עוד לפני שנוצרת הרצה כלשהי.

לכללים המלאים, ראו בדף ה-API [המשך הרצה שנכשלה](https://nodaro.ai/docs/developers/api/workflows#continue-a-failed-run).

בהתקנה שבה תצוגה מקדימה עוצרת את ההרצה לבדיקה, הרצה של תהליך שרכיב [החלת רשימת עריכה](https://nodaro.ai/docs/nodes/video/apply-edl#preview-and-render-final) שלו מוגדר לתצוגה מקדימה נדחית עם `400 preview_review_required`, כי אף אחד לא בודק הרצה מה-SDK. העבירו את הרינדור למצב סופי בהרצה הזו עם `inputOverrides: { [renderNodeId]: { quality: "final" } }`. ב-Nodaro Cloud, הרצה לא נעצרת בתצוגה מקדימה, ולכן הדחייה הזו לא קורה.

### renderFinal(id, params)‎
מרנדרת את הגרסה הסופית של רינדור ברכיב [החלת רשימת עריכה](https://nodaro.ai/docs/nodes/video/apply-edl#preview-and-render-final) שהפיק תצוגה מקדימה, אחרי שבדקתם אותה. המתודה מריצה את מה שהכפתור **רינדור סופי** בעורך מריץ: את הרינדור במצב סופי בהרצה הזו בלבד, ואז את כל הרכיבים שאחריו. הרכיב שומר על ההגדרה **איכות** שלו. כשרכיב מחליף מצלמות נמצא בין רכיב תוכנית עריכה לרינדור, רכיב מחליף מצלמות רץ שוב קודם.

```ts
renderFinal(id: string, params: { renderNodeId: string; continueFromExecutionId: string }): Promise<{ executionId: string; status: "pending" | "running" }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה התהליך." },
renderNodeId: { type: 'string', required: true, description: "רכיב הרינדור מסוג החלת רשימת עריכה שאת הגרסה הסופית שלו רוצים לרנדר." },
continueFromExecutionId: { type: 'string', required: true, description: "ההרצה שהפיקה את התצוגה המקדימה: הרצה שלכם של התהליך הזה שהושלמה." },
}}
/>

השרת מחשב אילו רכיבים ירוצו, לפי אותו כלל שהעורך משתמש בו, ולכן שולחים רק את הרינדור ואת ההרצה הקודמת. כל רכיב שלא רץ מעביר הלאה את מה שההרצה הקודמת הפיקה.

```ts
const { executionId } = await client.workflows.renderFinal(workflowId, {
renderNodeId,
continueFromExecutionId: previewRunId,
})
```

- מזהה של רכיב שלא קיים נדחה עם `render_final_node_not_found`, ורכיב שהוא לא רינדור של החלת רשימת עריכה נדחה עם `render_final_not_a_render`. גם הקודים של הרצת המשך ב-[`run()`](#runid-params) חלים כאן.
- כשהמשלם לא יכול לכסות את ההרצה, המתודה זורקת `InsufficientCreditsError` (`402`) עם `required` ו-`available`, עוד לפני שנוצרת הרצה כלשהי.
- אסימון OAuth צריך את ההרשאה `workflows:execute`.

### estimateRenderFinal(id, params)‎
נותנת אומדן מחיר ל-[`renderFinal()`](#renderfinalid-params) ולא יוצרת שום דבר. המתודה מקבלת את אותם פרמטרים.

```ts
estimateRenderFinal(id: string, params: { renderNodeId: string; continueFromExecutionId: string }): Promise<{ data: RenderFinalQuote }>
```

```ts
const { data: quote } = await client.workflows.estimateRenderFinal(workflowId, {
renderNodeId,
continueFromExecutionId: previewRunId,
})
if (quote.sufficient) {
await client.workflows.renderFinal(workflowId, { renderNodeId, continueFromExecutionId: previewRunId })
}
```

לטיפוס `RenderFinalQuote` יש `renderNodeId`, `nodeIds` (הרכיבים שירוצו), `inputOverrides` (הדריסה שההרצה רצה איתה), `estimatedCredits`, `holdCredits`, `sufficient` ו-`available`. השדה `holdCredits` הוא מה שמשוריין להרצה בזמן הרינדור, ואף פעם לא פחות מהאומדן: קליפ UGC נכלל בו לפי השריון שלו, בערך פי שניים מהמחיר שלו. השדה `sufficient` מציין אם המשלם יכול לכסות את השריון הזה, והשדה `available` הוא הקרדיטים שזמינים למשלם להוצאה. כשתקציב של סביבת עבודה משלם, הערך של `available` הוא `null`. במהדורה בלי קרדיטים, הערכים הם `null`. כשההרצה מריצה רכיב LLM, אומדן המחיר מוסיף `estimatedCreditsIsEstimate: true`: על הרכיב הזה משלמים לפי השימוש בפועל, ולכן הציגו את הסכום כ-`≈N`.

### export(workflowId, opts?)‎
מייצאת את הגרסה השמורה של תהליך כחבילת JSON ניידת, באותו פורמט קובץ שהתפריט **ייצוא** בעורך כותב. עם `assets: true`, החבילה כוללת גם את הנכסים שהתהליך משתמש בהם, כמו האפשרות **עם נכסים** בעורך.

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

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "התהליך לייצוא." },
assets: { type: 'boolean', default: 'false', description: "כולל את הדמויות, האובייקטים, היצורים והמיקומים שהתהליך משתמש בהם, כדי שהם ייווצרו מחדש בייבוא." },
}}
/>

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

**ניידות.** חבילה יכולה להצביע רק על מדיה שמופע אחר יכול להוריד. כשרכיבים משתמשים בכתובות שמופעים אחרים לא יכולים להגיע אליהן, כגון האחסון של התקנה באירוח עצמי ב-`localhost`, כתובת ברשת מקומית או שם `.internal`, החבילה מפרטת אותן תחת `portability.unreachableMedia`:

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

כשכל כתובות המדיה ציבוריות, השדה לא מופיע. חבילה כזו עדיין מיובאת, אבל הרכיבים האלה לא יכולים לרוץ במופע האחר עד שמעלים לשם את המדיה שוב. ראו את המדריך [ייבוא וייצוא](https://nodaro.ai/docs/guides/import-export).

### import(input)‎
מייבאת חבילה שיוצאה לאחד הפרויקטים שלכם ומחזירה את התהליך החדש.

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

<TypeTable
type={{
projectId: { type: 'string', required: true, description: "הפרויקט שאליו מייבאים." },
'...bundle': { type: 'WorkflowExport', required: true, description: "השדות של החבילה שיוצאה, שמועברים ישירות בתוך הקלט." },
}}
/>

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

מה הייבוא עושה:

- **הנכסים נוצרים מחדש.** הדמויות, האובייקטים, היצורים והמיקומים שבחבילה הופכים לפריטים חדשים בחשבון שלכם. התהליך מעודכן כך שיצביע עליהם: ברכיבי הנכסים שלו, בכל אזכור `@` בנתונים של הרכיבים ובהגדרות של התהליך.
- **המדיה מועתקת.** מדיה במארחים אחרים מועתקת לאחסון של המופע הזה כשאפשר להגיע אליה, כך שהתהליך לא תלוי בשרת של מישהו אחר. המגבלות הן 25 קבצים למדיה של התהליך ועוד 25 לנכסים שבחבילה, עם תמונות של עד 20 MB ווידאו או אודיו של עד 50 MB.
- **העותקים נספרים במכסת האחסון שלכם.** כשהאחסון שלכם נגמר, התהליך עצמו מיובא בכל זאת, והנכסים שלא נכנסו מופיעים בדוח.

השדה `importReport` מפרט מה קרה:

```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` מופיע בכל פעם שהחבילה כללה נכסים. השרת כבר עדכן כל אזכור בתוך התהליך. השתמשו במיפוי רק למזהים שאתם שומרים מחוץ לתהליך. השדה `assetsSkipped` מופיע רק כשמשהו הושמט.

### setVisibility(id, visibility)‎
מגדירה תהליך כ-`"private"`, גלוי ליוצר שלו ולשותפים שאתם מוסיפים, או כ-`"workspace"`, גלוי לכל מי שבסביבת העבודה שלו.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה התהליך." },
visibility: { type: '"private" | "workspace"', required: true, description: "מי יכול לראות את התהליך." },
}}
/>

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

רק היוצר או מנהל של סביבת העבודה יכולים לשנות את ההגדרה הזו; אצל כל אחד אחר הקריאה נכשלת עם `ForbiddenError`. סביבות עבודה קיימות בתוך [ארגונים](https://nodaro.ai/docs/developers/sdk/organizations) ב-Nodaro Cloud.

### move(id, params)‎
מעבירה תהליך לפרויקט אחר (`POST /v1/workflows/:id/move`). התהליך יוצא מהתיקייה שלו.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה התהליך." },
projectId: { type: 'string', required: true, description: "פרויקט היעד." },
}}
/>

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

כשההעברה מוציאה את התהליך מסביבת עבודה, הגישה שניתנה דרך סביבת העבודה הזו מסתיימת. רשימת האנשים האלה חוזרת ב-`droppedCollaborators`.

### sharedWithMe()‎
מחזירה את רשימת התהליכים שאנשים אחרים שיתפו איתכם ישירות. עבודה בסביבת עבודה שאתם חברים בה לא נכללת, כי היא כבר מופיעה ברשימות של סביבת העבודה עצמה. כל תהליך כולל את ה-`grantedRole` שיש לכם בו.

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

במופעים בלי ארגונים, הרשימה ריקה.

### listMcpConversations(params?)‎
מחזירה את רשימת שיחות ה-MCP שלכם בחודש אחד, מהחדש לישן, דרך `GET /v1/mcp-conversations`. כל שיחה היא תהליך שעוזר AI בנה מהשלבים שלה. ראו את הדף [איך שיחות MCP הופכות לתהליכים](https://nodaro.ai/docs/mcp/conversation-flows).

```ts
listMcpConversations(params?: { month?: string; sources?: string[] }): Promise<McpConversationList>
```

- הפרמטר `month` הוא `"YYYY-MM"`. ברירת המחדל היא החודש האחרון שיש בו שיחות.
- הפרמטר `sources` משאיר רק חלק מהמקורות, למשל `"claude"`, `"claude-code"` או `"chatgpt"`.

התוצאה כוללת את `month`, את `months` (כל חודש שיש בו שיחות, עם מספר השיחות בו), את `sources` (המקורות של החודש, וחיבורי ה-MCP שכל מקור מאחד) ואת `data`, מערך `McpConversation[]`. שיחה היא תהליך: פתחו אותה עם `get(conversation.id)`.

```ts
const { data, months } = await client.workflows.listMcpConversations({ month: "2026-10", sources: ["claude-code"] })
```

### collaborators
השותפים בתהליך, כלומר האנשים שהוא משותף איתם, זמינים דרך `client.workflows.collaborators`. המתודות האלה קיימות במופעים עם [ארגונים](https://nodaro.ai/docs/developers/sdk/organizations); בכל מופע אחר הן זורקות `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: "מזהה התהליך." },
userId: { type: 'string', description: "מזהה המשתמש של האדם. ב-add()‎, שלחו בדיוק אחד מהשניים, userId או email." },
email: { type: 'string', description: "כתובת האימייל של חשבון Nodaro כלשהו, בלי קשר לארגון שלו. כשאין חשבון עם הכתובת, המתודה זורקת NotFoundError." },
role: { type: '"viewer" | "editor"', required: true, description: "מה האדם רשאי לעשות בתהליך." },
}}
/>

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

שותף יכול לקרוא ל-`remove()` עם מזהה המשתמש שלו כדי לעזוב תהליך.

## client.projects
פרויקט מקבץ תהליכים. כל תהליך שייך לפרויקט אחד בדיוק.

### projects.list()‎
מחזירה את רשימת הפרויקטים שלכם.

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

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

### projects.get(id)‎
מחזירה פרויקט אחד.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה הפרויקט." },
}}
/>

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

### projects.create(input)‎
יוצרת פרויקט.

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

<TypeTable
type={{
name: { type: 'string', required: true, description: "שם הפרויקט." },
description: { type: 'string', description: "תיאור." },
settings: { type: 'Record<string, unknown>', description: "הגדרות הפרויקט." },
}}
/>

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

### projects.update(id, input)‎
משנה פרויקט. שלחו לפחות שדה אחד.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה הפרויקט." },
name: { type: 'string', description: "שם חדש." },
description: { type: 'string', description: "תיאור חדש." },
settings: { type: 'Record<string, unknown>', description: "הגדרות חדשות לפרויקט." },
}}
/>

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

### projects.delete(id)‎
מוחקת פרויקט.

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

<TypeTable
type={{
id: { type: 'string', required: true, description: "מזהה הפרויקט." },
}}
/>

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

## client.sessions
**סשן יצירה** מקבץ יצירות בודדות בתוך פרויקט, ומוצג באותה צורה בכל מכשיר. בפרויקט נמצאים הסשנים שלו, ובכל סשן נמצאים הפריטים שלו. הפרויקט של סשן נקבע כשיוצרים אותו. כל פריט הוא קישור לאחת המשימות שלכם, לא עותק.

```ts
const { data: session } = await client.sessions.create({ projectId, tool: "create-image", title: "Beach" })

// A job joins a session when the generation request names it.
await client.nodes.runAndWait("generate-image", {
prompt: "a red car",
sessionId: session.id,
origin: "create-image",
})

const { data: sessions, next } = await client.sessions.list({ projectId })
const { data: items } = await client.sessions.items(session.id)
```

| מתודה | חתימה | הערות |
| --- | --- | --- |
| `create` | `create(input: CreateSessionInput): Promise<{ data: GenerationSession }>` | השדה `tool` נדרש, למשל `create-image`. השדות `projectId` ו-`title` אופציונליים. |
| `list` | `list(params?: ListSessionsParams): Promise<SessionPage<GenerationSession>>` | מהחדש לישן. מסננים: `projectId` (`"none"` לסשנים שלא שייכים לאף פרויקט), `tool`, `includeArchived`, `limit` ו-`cursor`. כל סשן כולל את `itemCount` (הפריטים הפעילים), `removedCount` (הפריטים בפח), `coverJobId` ועד ארבע כתובות ב-`previewUrls` (תמונת השער קודם, כשהיא מוגדרת). |
| `get` | `get(id: string): Promise<{ data: GenerationSession }>` | סשן אחד. |
| `update` | `update(id: string, input: UpdateSessionInput): Promise<{ data: GenerationSession }>` | השדות `title` (הערך `null` משאיר את הסשן ללא שם), `archived` ו-`coverJobId` (אחד הפריטים של הסשן, אחרת 404; הערך `null` פירושו בחירה אוטומטית). אי אפשר לשנות את הפרויקט. |
| `delete` | `delete(id: string): Promise<{ success: true }>` | מוחקת את הסשן. המשימות שלו נשמרות, ונשארות בכל סשן אחר שהן נמצאות בו. |
| `items` | `items(id: string, params?: ListSessionItemsParams): Promise<SessionPage<SessionItem>>` | הפיד של הפריטים הפעילים, מהחדש לישן. לכל פריט יש `jobId`, `addedAt`, `removedAt`, `imported`, `importedFrom` ו-`job`. עם `{ removed: true }` מוחזרים במקום זאת הפריטים שהוסרו מהסשן, כלומר הפח של הסשן, מההסרה האחרונה לראשונה. |
| `importItems` | `importItems(id: string, input: { jobIds: string[] }): Promise<{ data: { imported: number; requested: number } }>` | מקשרת לסשן עד 100 מהמשימות הפעילות **שלכם**. זה הכול או כלום: משימה אחת שלא שייכת לכם מכשילה את הקריאה עם 404. פריט שהוסר מהסשן חוזר למקום שבו היה. |
| `moveItems` | `moveItems(targetId: string, input: MoveSessionItemsInput): Promise<{ data: { moved: number } }>` | `{ jobIds, fromSessionId }`, בטרנזקציה אחת. סשן היעד מקבל את הפריטים, וסשן המקור שומר אותם כפריטים שהוסרו, כך שהמתודה `restoreItem(fromSessionId, jobId)` מבטלת את ההעברה. זה הכול או כלום: 404 כשמזהה כלשהו הוא לא פריט פעיל בסשן המקור. |
| `removeItem` | `removeItem(id: string, jobId: string, options?: { forever?: boolean }): Promise<{ success: true }>` | מעבירה את הפריט לפח של הסשן, עד שמשחזרים אותו. שום דבר לא מתנקה אוטומטית. המשימה, וכל סשן אחר שהיא נמצאת בו, נשארים. עם `{ forever: true }`, המתודה מוחקת לצמיתות פריט שכבר הוסר, ועל פריט פעיל היא זורקת `NodaroError` עם 409 ו-`not_in_trash`. |
| `restoreItem` | `restoreItem(id: string, jobId: string): Promise<{ success: true }>` | מחזירה פריט שהוסר לסשן, למקום שבו היה. |

הפרויקט של סשן קבוע, ולכן כדי להעביר פריטים לפרויקט אחר, מעבירים אותם לסשן של אותו פרויקט: קראו ל-`moveItems()` על סשן שם. פריט שכבר לא נמצא באף סשן מופיע תחת **כללי**: `client.jobs.list({ inSession: false })`.

## client.trash
בפח נמצאים הפריטים שהסרתם מסשנים והמשימות שמחקתם. כלי יצירה מציג אותו כרשומה אחת לצד **כללי**. ראו בדף ה-API [הפח](https://nodaro.ai/docs/developers/api/jobs#the-trash).

```ts
list(params?: ListTrashParams): Promise<TrashPage> // GET /v1/trash

interface ListTrashParams {
projectId?: string | "none" // a project, or the sessions with no project and General
tool?: string               // what a session of this tool holds or held
type?: string | string[]    // input_data.type, like jobs.list
limit?: number              // 1–100 (default 50)
cursor?: string             // the previous page's next
}

type TrashEntry =
| { kind: "removed"; removedAt: string; session: TrashSession; job: Job }
| { kind: "deleted"; deletedAt: string; purgeAt: string; job: Job }
```

הרשימה מסודרת מהחדש לישן. פריט **שהוסר** נשאר עד שקוראים ל-`sessions.restoreItem(session.id, job.id)` או ל-`sessions.removeItem(session.id, job.id, { forever: true })`. משימה **שנמחקה** נשארת עד `purgeAt`, 30 ימים אחרי `deletedAt`, ואז נמחקת לצמיתות. עותק שנשמר בספרייה נשאר. המתודה `jobs.restore(job.id)` מחזירה אותה לכל סשן שהיא הייתה בו, והמתודה `jobs.delete(job.id, { forever: true })` מוחקת אותה מיד. פריט שהוסר, ושגם את המשימה שלו מחקתם, מופיע פעם אחת, כמשימה שנמחקה. למשימה שאף פעם לא הייתה בסשן אין כלי, ולכן הסינון לפי `tool` משמיט אותה: כדי לכלול אותה, שלחו `type` עם סוגי המשימות של הכלי שלכם.

```ts
const { data: trash } = await client.trash.list({ projectId, type: ["generate-image", "image-edit"] })
for (const entry of trash) {
if (entry.kind === "deleted") await client.jobs.restore(entry.job.id)
else await client.sessions.restoreItem(entry.session.id, entry.job.id)
}
```

כל בקשה שיוצרת משימה מקבלת `sessionId` ו-`origin`, והפלטפורמה רושמת אותם במשימה. הם מופיעים בטיפוסים של כל פרמטרי היצירה, כגון `GenerateImageParams`, `GenerateVideoParams` ו-`TextToSpeechParams`, ובקלטי ה-`generate*` של דמויות, אובייקטים, מיקומים ויצורים. הערך של `origin` חייב להיות slug: אותיות לטיניות קטנות, ספרות ומקפים, עד 40 תווים. כל ערך אחר נדחה עם `400 validation_error`. השדה `session_id` של משימה מופיע ב-`client.jobs.get()` וב-`client.jobs.list()`.

## טיפוסים
| טיפוס | מבנה |
| --- | --- |
| `Workflow` | `id`, `projectId`, `userId`, `name`, `description?`, `folderId?`, `version?`, `thumbnailUrl?`, `nodes?`, `edges?`, `settings?`, `sourcePrompt?`, `createdAt`, `updatedAt`. שדות הגרף מופיעים רק ברשומות מלאות. |
| `RunWorkflowParams` | `nodeIds?`, `inputOverrides?`, `continueFromExecutionId?` |
| `RenderFinalParams` | `renderNodeId`, `continueFromExecutionId` |
| `RenderFinalQuote` | `renderNodeId`, `nodeIds`, `inputOverrides`, `estimatedCredits`, `estimatedCreditsIsEstimate?`, `holdCredits`, `sufficient`, `available` |
| `Project` | `id`, `userId`, `name`, `description?`, `settings?`, `createdAt`, `updatedAt` |
| `GenerationSession`, `SessionItem`, `SessionPage` | סשן יצירה, פריט אחד שלו ועמוד של כל אחד מהם |
| `Collaborator` | `userId`, `name?`, `avatar?`, `role` |
| `WorkflowExport` | החבילה הניידת שהמתודה `export()` מחזירה והמתודה `import()` מקבלת |
| `GenericNode`, `GenericEdge` | המבנים של רכיב ושל חיבור שמשמשים ב-`nodes` וב-`edges` |

כולם מיוצאים מ-`@nodaro/sdk`. ראו את הדף [טיפוסים](https://nodaro.ai/docs/developers/sdk/types).

## שאלות נפוצות

### איך מריצים תהליך Nodaro מקוד?

קראו למתודה client.workflows.run עם מזהה התהליך. המתודה מחזירה מיד executionId. בדקו את הסטטוס מדי כמה שניות עם המתודה client.executions.get ועם ה-executionId, עד שהסטטוס הוא completed, failed, cancelled או timed_out.

### האם אפשר להריץ רק חלק מהרכיבים של תהליך?

כן. העבירו לארגומנט השני של המתודה client.workflows.run אובייקט עם nodeIds, רשימת המזהים של הרכיבים שרוצים להריץ. רק הרכיבים האלה רצים.

### איך מרנדרים מקוד את הגרסה הסופית של תצוגה מקדימה?

קראו למתודה client.workflows.renderFinal עם המזהה של רכיב החלת רשימת עריכה שמבצע את הרינדור, ועם המזהה של ההרצה שהפיקה את התצוגה המקדימה. המתודה מריצה את הרינדור במצב סופי ואת כל הרכיבים שאחריו. המתודה client.workflows.estimateRenderFinal נותנת אומדן מחיר לפני כן.

### איך נמנעים מדריסה של שינויים שמישהו אחר עשה?

העבירו למתודה client.workflows.update את expectedVersion, הגרסה שקראתם. אם התהליך השתנה בינתיים, המתודה זורקת WorkflowConflictError עם הרשומה הנוכחית, כך שאפשר למזג ולנסות שוב.

### איך מעתיקים תהליך למופע Nodaro אחר?

ייצאו את התהליך עם המתודה client.workflows.export, כשהאפשרות assets מוגדרת ל-true, ואז ייבאו את החבילה במופע האחר עם המתודה client.workflows.import ועם projectId. מדיה נגישה מועתקת לאחסון של המופע החדש.
