# אפליקציות ותבניות

> עיון באפליקציות Nodaro שפורסמו והרצה שלהן מקוד TypeScript, קריאת היסטוריית ההרצות, שכפול תבניות של תהליכים לפרויקט והצגת המדריכים.

Source: https://nodaro.ai/he/docs/developers/sdk/apps-and-templates

**`client.apps`** מאפשר לעיין באפליקציות שפורסמו ולהריץ אותן: תהליכים שעטופים בטופס פשוט של קלטים ופלטים. המשאב **`client.templates`** מאפשר לעיין בשוק התבניות ולשכפל תבנית לאחד הפרויקטים שלכם, והמשאב **`client.tutorials`** מציג את מדריכי הווידאו ומדריכי התהליכים. המושגים מוסברים בדפים [אפליקציות](https://nodaro.ai/docs/concepts/apps) ו[מדריכים ותבניות](https://nodaro.ai/docs/get-started/tutorials-and-templates).

## מתודות
| מתודה | מה היא עושה |
| --- | --- |
| [`apps.list(params?)`](#appslistparams) | עיון באפליקציות שפורסמו |
| [`apps.get(slug)`](#appsgetslug) | קריאת אפליקציה עם הקלטים והפלטים שלה |
| [`apps.run(slug, inputs?, opts?)`](#appsrunslug-inputs-opts) | הרצת אפליקציה |
| [`apps.listRuns(slug, params?)`](#appslistrunsslug-params) | רשימת ההרצות שלכם של אפליקציה |
| [`apps.getRun(slug, runId)`](#appsgetrunslug-runid) | קריאת הרצה אחת |
| [`apps.resume(slug, runId, opts?)`](#appsresumeslug-runid-opts) | ניסיון חוזר של הרצה שנכשלה, מהמקום שבו היא נעצרה |
| [`apps.deleteRun(slug, runId)`](#appsdeleterunslug-runid) | העברת הרצה לארכיון |
| [`templates.browse(params?)`](#templatesbrowseparams) | עיון בשוק התבניות |
| [`templates.get(slug)`](#templatesgetslug) | קריאת תבנית עם התהליך שלה |
| [`templates.clone(slug, params)`](#templatescloneslug-params) | העתקת תבנית לפרויקט |
| [`tutorials.list()`](#tutorialslist) | רשימת המדריכים לפי קטגוריה |

## client.apps
המתודות `list()` ו-`get()` הן ציבוריות. המתודה `run()` ומתודות היסטוריית ההרצות פועלות בשם המשתמש המחובר שקורא להן.

### apps.list(params?)‎
מחזירה את האפליקציות שפורסמו, עמוד אחד בכל פעם.

```ts
list(params?: { search?: string; category?: string; limit?: number; cursor?: string }): Promise<{
data: PublishedApp[]
nextCursor?: string | null
}>
```

<TypeTable
type={{
search: { type: 'string', description: "מילים לחיפוש." },
category: { type: 'string', description: "רק אפליקציות מהקטגוריה הזו." },
limit: { type: 'number', description: "גודל העמוד, עד 50." },
cursor: { type: 'string', description: "הערך nextCursor מהעמוד הקודם." },
}}
/>

```ts
const { data: apps, nextCursor } = await client.apps.list({ search: "headshot", limit: 20 })
```

השדות של `PublishedApp` הם `id`, `slug`, `name`, `description`, `creatorId`, `creatorName`, `thumbnailUrl`, `category`, `isFeatured`, `runCount`, `estimatedCredits`, `perMinuteCredits`, `perItemCredits`, `createdAt` ו-`updatedAt`.

השדה `estimatedCredits` הוא המחיר הרשום של האפליקציה להרצה אחת. המחיר של חלק מהאפליקציות נקבע לפי האורך של ההקלטה שנותנים להן, כמו מעבר של רכיב תוכנית עריכה או רינדור של פרק שלם. באפליקציות כאלה, הערך של `perMinuteCredits` גדול מ-0, והמחיר הוא `estimatedCredits` ועוד `perMinuteCredits` לכל דקה של ההקלטה. כשלאפליקציה יש קלט רשימה שהמשתמש שלה ממלא, הערך של `perItemCredits` גדול מ-0: כל פריט מעבר לפריטים שהיוצר שמר מוסיף את מספר הקרדיטים הזה.

המחיר הרשום הוא טווח כשבחירה שעומדת לרשות המשתמש של האפליקציה עולה יותר. השדה `maxEstimatedCredits`, יחד עם `maxPerMinuteCredits` ו-`maxPerItemCredits`, מציין את הסכום הגבוה ביותר שהרצה יכולה לעלות בכל השדות שהאפליקציה חושפת, בתוך המגבלות שהיוצר הגדיר. השדה `estimatedCredits` והחלקים שלו הם המחיר בהגדרות השמורות, מחיר ה״החל מ״. שדות ה-`max*` מופיעים רק כשהם שונים. סרטון UGC מתומחר מחדש לכל צירוף, ולכן אורך תסריט או שפה שהמשתמש של האפליקציה יכול לבחור נספרים לפי המחיר הגבוה ביותר שלהם. לאפליקציה שפורסמה לפני שהטווח נוסף אין טווח עד הפרסום הבא שלה.

הערך `estimatedCreditsIsEstimate: true` מסמן מחיר רשום שכולל אומדן של קריאה ל-LLM שמחויבת לפי שימוש. השדה מופיע באפליקציות, בתבניות ובמדריכים. הקריאה ל-LLM מחויבת לפי השימוש בפועל בטוקנים, ולכן הציגו את המחיר כ-`≈N`. במחיר קבוע השדה לא מופיע. פרסום של תהליך שמציין מודל LLM שעדיין אין לו מחיר נדחה עם `400 validation_error` ועם `reason: "llm_model_unpriced"`. ראו בדף ה-API [מודל שעדיין אין לו מחיר](https://nodaro.ai/docs/developers/api/errors#llm-model-unpriced). כשאי אפשר לתמחר את סרטון ה-UGC של אפליקציה שמפרסמים, בהגדרות השמורות או בבחירה שעומדת לרשות המשתמש של האפליקציה, האפליקציה אף פעם לא נרשמת כחינמית. הפרסום נדחה עם `503`, כשגם הקוד וגם ה-`reason` הם `ugc_listing_unpriced`, ועם `choice` שמציין מה נכשל, למשל `"a choice its users can make (Hebrew, 60 s)"` (בחירה שהמשתמשים שלה יכולים לעשות: עברית, 60 שניות). שום דבר לא נכתב, ולכן פרסמו שוב. דרך ה-SDK, זו שגיאת `NodaroError` עם ה-`code` וה-`status` האלה.

### apps.get(slug)‎
מחזירה אפליקציה אחת, עם `inputSchema` (השדות שמשתמשי הקצה ממלאים) ועם `outputs` (מה שהאפליקציה מחזירה).

```ts
get(slug: string): Promise<{ data: PublishedAppDetail }>
```

<TypeTable
type={{
slug: { type: 'string', required: true, description: "המזהה בכתובת (slug) של האפליקציה." },
}}
/>

```ts
const { data: app } = await client.apps.get("pro-headshot")
console.log(app.inputSchema, app.outputs) // outputs: [{ nodeId, label, type }]
```

השדות `runEstimatedCredits`, `runPerMinuteCredits` ו-`runPerItemCredits` הם המחיר של הרצת האפליקציה לבדה: המחיר הרשום בלי החלק של [רינדור סופי](https://nodaro.ai/docs/nodes/video/apply-edl#preview-and-render-final), שרץ ומשלמים עליו בנפרד. השדה `editPlanPerMinute` הוא `true` כשעל רכיב תוכנית עריכה משלמים לכל דקה של ההקלטה, או חלק ממנה, ו-`false` כשמשלמים עליו במדרגות של 15, 30, 60, 90, 120 או 180 דקות.

### apps.run(slug, inputs?, opts?)‎
מריצה אפליקציה ומחזירה מיד `{ executionId, status, runId? }`. כדי לקבל את התוצאה, קראו ל-[`client.executions.get(executionId)`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) מדי כמה שניות.

```ts
run(slug: string, inputs?: Record<string, unknown>, opts?: {
inputOverrides?: Record<string, Record<string, unknown>>
}): Promise<{ executionId: string; status: "pending" | "running"; runId?: string }>
```

<TypeTable
type={{
slug: { type: 'string', required: true, description: "המזהה בכתובת של האפליקציה." },
inputs: { type: 'Record<string, unknown>', description: "ערכי הקלט של האפליקציה. המפתחות תואמים לשמות השדות ב-inputSchema." },
inputOverrides: { type: 'Record<string, Record<string, unknown>>', description: "הגדרות גולמיות של רכיבים להרצה הזו בלבד, לפי מזהה רכיב, למשל ‎{ n1: { promptPrefix: '...' } }‎. הן מגיעות לשדות שהאפליקציה לא מציגה למשתמשים שלה." },
}}
/>

```ts
const { executionId } = await client.apps.run("pro-headshot", { photo: photoUrl })

// Add hidden text before the app's prompt, for this run only
await client.apps.run(
"pro-headshot",
{ photo: photoUrl },
{ inputOverrides: { n1: { promptPrefix: "Studio portrait of" } } },
)
```

כשאפליקציה מקבלת קישור לסרטון דרך רכיב [כתובת וידאו](https://nodaro.ai/docs/nodes/automate/video-url#use-it-as-an-app-input) שהיא חושפת, מריצים אותה עם הקישור כערך הקלט, למשל `client.apps.run("clip-pack", { episode: "https://www.youtube.com/watch?v=..." })`. השרת מוריד את הסרטון מקישור לפוסט לפני שהרכיב הראשון רץ. סרטון יוטיוב של 4 דקות או יותר יורד רק כקטע שאתם מציינים ב-`inputOverrides`. כל קישור אינטרנט אחר משמש כמו שהוא.

האפשרות `inputOverrides` מיועדת לשימוש מתקדם. היא יכולה לקבוע, למשל, את [הטקסט לפני ואחרי](https://nodaro.ai/docs/concepts/prompt-pre-post-text) של רכיב (`promptPrefix` ו-`promptSuffix`). היא לא יכולה לקבוע יעד של רכיב פלט, כמו הכתובת של רכיב פלט Webhook, חשבון לפרסום או היעד של רכיב גריפה. דריסה כזו נדחית עם `400 locked_field`.

### apps.listRuns(slug, params?)‎
מחזירה את ההרצות שלכם של אפליקציה, עמוד אחד בכל פעם.

```ts
listRuns(slug: string, params?: { limit?: number; cursor?: string }): Promise<{ data: AppRun[]; nextCursor?: string | null }>
```

<TypeTable
type={{
slug: { type: 'string', required: true, description: "המזהה בכתובת של האפליקציה." },
limit: { type: 'number', description: "גודל העמוד." },
cursor: { type: 'string', description: "הערך nextCursor מהעמוד הקודם." },
}}
/>

```ts
const { data: runs } = await client.apps.listRuns("pro-headshot", { limit: 10 })
for (const run of runs) console.log(run.status, run.outputs)
```

השדות של `AppRun` הם `id`, `appSlug`, `executionId`, `status` (`pending`, `running`, `completed`, `failed` או `cancelled`), `inputs`, `outputs` (כל פלט הוא `{ nodeId, type, url?, text? }`), `startedAt` ו-`finishedAt`.

### apps.getRun(slug, runId)‎
מחזירה הרצה אחת של אפליקציה.

```ts
getRun(slug: string, runId: string): Promise<{ data: AppRun }>
```

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

```ts
const { data: run } = await client.apps.getRun("pro-headshot", runId)
```

### apps.resume(slug, runId, opts?)‎
מנסה שוב הרצה **שנכשלה**, מהמקום שבו היא נעצרה. זה הכפתור **נסו שוב** בדף של האפליקציה. רק השלבים שההרצה לא השלימה רצים, ורק עליהם משלמים. כל שלב שההרצה השלימה מעביר הלאה את התוצאה שלו. רכיב [קליפ UGC](https://nodaro.ai/docs/nodes/video/ugc-clip) שנכשל מרנדר רק את הקליפים שהוא לא סיים.

```ts
resume(slug: string, runId: string, opts: { dryRun: true; version?: number }): Promise<{ data: AppRunResumePlan }>
resume(slug: string, runId: string, opts?: { dryRun?: false; version?: number }): Promise<AppRunResumeResult>
```

<TypeTable
type={{
slug: { type: 'string', required: true, description: "המזהה בכתובת של האפליקציה." },
runId: { type: 'string', required: true, description: "המזהה של ההרצה שנכשלה." },
'opts.dryRun': { type: 'boolean', description: "התשובה מתארת מה הניסיון החוזר יעשה, ושום דבר לא מתחיל." },
'opts.version': { type: 'number', description: "גרסת האפליקציה שאתם מצפים שהניסיון החוזר יריץ, כלומר הגרסה שהתחלה מחדש של ההרצה הייתה מריצה. ברירת המחדל: הגרסה האחרונה של האפליקציה." },
}}
/>

- **הגרסה והקלטים של ההרצה עצמה.** הניסיון החוזר מריץ את גרסת האפליקציה שההרצה הריצה, עם הקלטים שההרצה השתמשה בהם. הוא לא מקבל קלטים משלו.
- **ההרצה היבשה.** עם `dryRun: true`, התשובה היא `AppRunResumePlan`: `runId`, `continuesExecutionId`, `appVersion`, `nodeIds`, `steps` (`{ nodeId, type, label }`, השלבים שרצים שוב), `estimatedCredits`, `reusedClips`, `sufficient` ו-`available`. השדה `estimatedCredits` כולל את תוספת המחיר של היוצר על האפליקציה, והוא `null` כשאין קרדיטים. השדה `sufficient` מציין אם היתרה שלכם והקרדיטים לאפליקציות שלכם מכסים את הניסיון החוזר. השדה `appCreditsAllowance` מופיע רק כשהקרדיטים לאפליקציות הם מה שעוצר אותו. הציגו את התוכנית הזו לפני ניסיון חוזר.
- **ניסיון חוזר שהתחיל.** בלי `dryRun`, התשובה היא `{ executionId, runId, status: "pending", nodeIds, deduped? }`. בדקו את הסטטוס שלו מדי כמה שניות עם `client.executions.get(executionId)`. ההרצה שמאחורי הרצת האפליקציה היא מעכשיו זו של הניסיון החוזר, ולכן המתודה `getRun` מציגה אותה.
- **ניסיון חוזר אחד בכל פעם.** בקריאה שנייה, בזמן שהניסיון החוזר עדיין ממתין, המתודה זורקת `AlreadyRunningError` שמציינת את ה-`executionId` שלו. רק שתי לחיצות באותו רגע בדיוק מגיעות לאותו ניסיון חוזר, והתשובה להן כוללת `deduped: true`.

הדחיות מגיעות לפני שמשהו נכתב ולפני שנגבים קרדיטים. השדה `code` הוא אחד מהערכים של `AppRunResumeRefusalCode`:

| `code` | השגיאה שנזרקת | מתי |
| --- | --- | --- |
| `not_found` | `NotFoundError` | אין לכם הרצה כזו באפליקציה הזו. |
| `continuation_not_found` | `NotFoundError` (השדה `code` שלה הוא `not_found`) | ההרצה שמאחורי הרצת האפליקציה לא שייכת לכם. |
| `validation_error` | `NodaroError` (400) | מזהה הרצה או גוף בקשה פגומים, או הרצה של רכיב שמור. |
| `app_run_version_unavailable` | `NodaroError` (409) | הגרסה שההרצה הריצה כבר לא קיימת. התחילו את ההרצה מחדש. |
| `app_run_version_changed` | `AppRunVersionChangedError` | האפליקציה בגרסה אחרת מזו שההרצה הריצה. התחילו מחדש, או שלחו `version: err.runVersion`. |
| `continuation_workflow_mismatch` / `continuation_version_mismatch` | `NodaroError` (400) | ההרצה שמאחורי הרצת האפליקציה הריצה תהליך אחר, או גרסה אחרת שלו. התחילו את ההרצה מחדש. |
| `continuation_not_failed` | `NodaroError` (409) | ההרצה הושלמה או נעצרה. התחילו אותה מחדש. |
| `already_running` | `AlreadyRunningError` (409, `executionId`) | ההרצה עדיין רצה, או שניסיון חוזר שלה ממתין. בדקו את המצב של `executionId`. |
| `continuation_subset_required` | `NodaroError` (409, או 400) | לא נשאר מה לנסות שוב (409), או שהאפליקציה לא מריצה אף שלב (400). |
| `locked_field` | `NodaroError` (400) | קלט שההרצה השתמשה בו קובע שדה שהאפליקציה לא מאפשרת למי שמריץ אותה לקבוע. |
| `preview_review_required` | `NodaroError` (400) | הניסיון החוזר היה נעצר בתצוגה מקדימה, ואין מי שיבדוק אותה. נסו שוב מהדף של האפליקציה. |
| `insufficient_app_credits` / `insufficient_credits` | `InsufficientCreditsError` | הקרדיטים לאפליקציות או היתרה שלכם לא מספיקים לניסיון החוזר. השדה `code` של השגיאה מציין איזה מהשניים. |
| `billing_unavailable` | `NodaroError` (503) | החיוב לא זמין לרגע בהרצה בסביבת עבודה. נסו שוב בעוד רגע. |

כדי להריץ עם קלטים אחרים או בגרסה הנוכחית של האפליקציה, התחילו את ההרצה מחדש עם `apps.run`. אז כל השלבים רצים שוב, ומשלמים על כולם שוב. ראו את הסעיף [ניסיון חוזר של הרצה שנכשלה](https://nodaro.ai/docs/developers/embed/miniapps#retry-a-failed-run) בדף הטמעת מיני־אפליקציה.

```ts

const { data: plan } = await client.apps.resume("pro-headshot", runId, { dryRun: true })
console.log(`${plan.steps.length} step(s) again, about ${plan.estimatedCredits} credits`)

try {
const { executionId } = await client.apps.resume("pro-headshot", runId)
} catch (err) {
if (err instanceof AppRunVersionChangedError && err.runVersion !== undefined) {
// Continue on the run's own version anyway, or start over with apps.run.
await client.apps.resume("pro-headshot", runId, { version: err.runVersion })
}
}
```

### apps.deleteRun(slug, runId)‎
מעבירה הרצה לארכיון. שחזור של הרצה ומחיקה לצמיתות שלה זמינים רק באפליקציה של Nodaro, כך שסקריפט לא יכול להשמיד נתונים.

```ts
deleteRun(slug: string, runId: string): Promise<{ success: true; archived: true }>
```

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

```ts
await client.apps.deleteRun("pro-headshot", runId)
```

## client.templates
שוק התבניות, שהוא ציבורי בכוונה: אפשר לעיין בו, לקרוא תבנית עם כל התהליך שלה ולשכפל אותה לפרויקט שלכם בחינם.

### templates.browse(params?)‎
מחזירה תבניות מהשוק, עמוד אחד בכל פעם (`GET /v1/templates/browse`). אין צורך להתחבר.

```ts
browse(params?: BrowseTemplatesParams): Promise<{ data: TemplateBrowseCard[]; nextCursor: string | null }>
```

<TypeTable
type={{
search: { type: 'string', description: "חיפוש בטקסט מלא." },
category: { type: 'string', description: "רק תבניות מהקטגוריה הזו." },
outputType: { type: 'string', description: "רק תבניות שמפיקות סוג פלט כזה." },
tag: { type: 'string', description: "רק תבניות עם התגית הזו." },
nodeType: { type: 'string', description: "רק תבניות שמשתמשות בסוג הרכיב הזה." },
provider: { type: 'string', description: "רק תבניות שמשתמשות במודל הזה." },
complexity: { type: 'string', description: "רק תבניות ברמת המורכבות הזו." },
sort: { type: '"newest" | "popular" | "most-favorited"', default: '"newest"', description: "סדר המיון. הנתיב מקבל גם cheapest, ראו בהמשך." },
limit: { type: 'number', description: "גודל העמוד." },
cursor: { type: 'string', description: "הערך nextCursor מהעמוד הקודם." },
}}
/>

```ts
const page = await client.templates.browse({ sort: "popular", search: "trailer" })
```

השדה `estimatedCredits` של כרטיס הוא המחיר הרשום של התבנית. כשהמחיר נקבע לפי האורך של ההקלטה שנותנים לתבנית, הערך של `estimatedPerMinuteCredits` גדול מ-0. המחיר הוא אז `estimatedCredits` ועוד מספר הקרדיטים הזה לכל דקה של ההקלטה. אחרת, השדה `estimatedPerMinuteCredits` לא מופיע או שהוא `0`. כשבחירה בתבנית יכולה לעלות יותר, הכרטיס כולל גם את `maxEstimatedCredits` ואת `maxEstimatedPerMinuteCredits`, הקצה העליון של טווח המחירים.

הנתיב מקבל גם `sort=cheapest`, סדר המיון **הכי פחות קרדיטים** של דף התבניות. המיון הוא לפי המחיר של הקלטה של 60 דקות, מהזול ליקר: הערך של `estimatedCredits` ועוד 60 כפול `estimatedPerMinuteCredits`. הטיפוס `TemplateSort` של ה-SDK לא כולל את `cheapest`, ולכן שלחו אותו עם [`client.request()`](https://nodaro.ai/docs/developers/sdk/client#requestmethod-path-options).

### templates.get(slug)‎
מחזירה תבנית ציבורית אחת עם תמונת המצב המלאה של התהליך שלה: `snapshotNodes`, `snapshotEdges` ו-`snapshotSettings` (`GET /v1/templates/:slug`).

```ts
get(slug: string): Promise<Template>
```

<TypeTable
type={{
slug: { type: 'string', required: true, description: "המזהה בכתובת של התבנית." },
}}
/>

```ts
const template = await client.templates.get("noir-trailer")
console.log(template.snapshotNodes.length)
```

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

### templates.clone(slug, params)‎
מעתיקה תבנית לאחד הפרויקטים שלכם כתהליך חדש (`POST /v1/templates/:slug/clone`). השכפול לא עולה קרדיטים.

```ts
clone(slug: string, params: { projectId: string; name?: string }): Promise<{ workflowId: string; projectId: string }>
```

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

```ts
const { workflowId } = await client.templates.clone("noir-trailer", { projectId })
await client.workflows.run(workflowId)
```

## client.tutorials
### tutorials.list()‎
מחזירה את כל קטגוריות המדריכים, ובכל אחת את מדריכי הווידאו ומדריכי התהליכים שלה (`GET /v1/tutorials`). המתודה ציבורית ולקריאה בלבד.

```ts
list(): Promise<{ categories: TutorialCategory[] }>
```

```ts
const { categories } = await client.tutorials.list()
for (const category of categories) {
console.log(category.name, category.videos.length, category.flows.length)
}
```

השדות של כל קטגוריה הם `id`, `name`, `slug`, `sortOrder`, `videos` ו-`flows`. לכל סרטון יש `title`, `videoUrl` ו-`thumbnailUrl`. כל פריט ב-`flows` הוא תבנית שמסומנת כמדריך: יש בו `title`, `complexity`, `estimatedCredits`, סוגי הרכיבים והמודלים שהוא משתמש בהם, ו-`slug` שאפשר להעביר ל-`templates.get()` ול-`templates.clone()`.

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

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

כדי להריץ אפליקציה מקוד, קראו ל-client.apps.run עם המזהה בכתובת של האפליקציה ועם אובייקט inputs שהמפתחות שלו תואמים לשדות הקלט של האפליקציה. המתודה מחזירה executionId. כדי לקבל את התוצאה, קראו ל-client.executions.get(executionId)‎ מדי כמה שניות.

### איך מגלים אילו קלטים אפליקציה מצפה לקבל?

כדי לגלות את הקלטים של אפליקציה, קראו ל-client.apps.get(slug)‎. ב-inputSchema של האפליקציה מפורטים השדות שמשתמשי הקצה ממלאים, וב-outputs מפורט מה האפליקציה מחזירה.

### האם שכפול תבנית עולה קרדיטים?

לא. שכפול תבנית עם client.templates.clone מעתיק את התבנית לאחד הפרויקטים שלכם בחינם. משלמים רק כשמריצים את התהליך החדש.

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

כדי לנסות שוב הרצת אפליקציה שנכשלה, קראו ל-client.apps.resume(slug, runId, { dryRun: true })‎ כדי לראות את השלבים ואת המחיר, ואז קראו למתודה שוב בלי dryRun. רק השלבים שההרצה לא השלימה רצים, ורק עליהם משלמים.

### האם inputOverrides יכול לשנות לאן אפליקציה שולחת את התוצאות שלה?

לא. הפרמטר inputOverrides לא יכול לקבוע יעד, כמו הכתובת של רכיב פלט Webhook או חשבון לפרסום. הרצה שמנסה לעשות זאת נדחית עם 400 locked_field.
