NodaroAccount
Nodaro Docs
DocumentationNode ReferenceModelsTemplatesAI Agents (MCP)DevelopersSelf-hostingResearchCompare
TypeScript SDK

Apps and templates

Browse and run published Nodaro apps from TypeScript, read their run history, clone workflow templates into a project, and list the tutorials.

client.apps browses and runs published apps: workflows wrapped in a simple form of inputs and outputs. client.templates browses the template marketplace and clones a template into one of your projects, and client.tutorials lists the tutorial videos and tutorial workflows. See Apps and Tutorials and templates for the concepts.

Methods

MethodWhat it does
apps.list(params?)Browse published apps
apps.get(slug)Read an app with its inputs and outputs
apps.run(slug, inputs?, opts?)Run an app
apps.listRuns(slug, params?)List your runs of an app
apps.getRun(slug, runId)Read one run
apps.resume(slug, runId, opts?)Retry a failed run from where it stopped
apps.deleteRun(slug, runId)Archive a run
templates.browse(params?)Browse the template marketplace
templates.get(slug)Read a template with its workflow
templates.clone(slug, params)Copy a template into a project
tutorials.list()List the tutorials by category

client.apps

list() and get() are public. run() and the run history act as the signed-in caller.

apps.list(params?)

Browses published apps, a page at a time.

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

Prop

Type

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

The method calls GET /v1/apps/browse. A deployment can turn off public work (outputs.allowPublic: false). There, the marketplace lists nothing: { data: [], nextCursor: null }, and an app is its creator's alone. get(), run() and resume() of someone else's app throw NotFoundError, as for an app that does not exist. Your own past runs of it stay readable with listRuns() and getRun().

A PublishedApp has id, slug, name, description, creatorId, creatorName, thumbnailUrl, category, isFeatured, runCount, estimatedCredits, perMinuteCredits, perItemCredits, createdAt and updatedAt.

estimatedCredits is the app's listed price per run. Some apps are priced by the length of the recording they are given, such as an Edit Plan pass or the render of a whole episode. For them, perMinuteCredits is above 0, and the price is estimatedCredits plus perMinuteCredits for each minute of the recording. When the app has a list input that its user fills, perItemCredits is above 0: each item beyond the creator's saved items adds that many credits.

The listed price is a range when a choice the app's user can make costs more. maxEstimatedCredits, with maxPerMinuteCredits and maxPerItemCredits, is the most a run can cost over the app's exposed fields, within any limits the creator set. estimatedCredits and its parts are the saved settings, the "from" price. The max* fields are present only when they differ. A UGC video is priced again for each combination, so a script length or language the app's user can choose counts at its highest price. An app published before the range existed has none until its next publish.

estimatedCreditsIsEstimate: true marks a listed price that holds a metered LLM call's estimate. Apps, templates and tutorials carry it. The LLM call is charged its actual usage per token, so show the price as ≈N. The field is absent for a fixed price. A publish whose workflow names an LLM model with no price yet is refused with 400 validation_error and reason: "llm_model_unpriced". See A model with no price yet. A publish whose UGC video could not be priced, at the saved settings or for a choice the app's user can make, is never listed as free. It is refused with 503, with code and reason ugc_listing_unpriced, and a choice that names what failed, for example "a choice its users can make (Hebrew, 60 s)". Nothing is written, so publish again. Read through the SDK, it is a NodaroError with that code and status.

apps.get(slug)

Reads one app, with its inputSchema, the fields end users fill in, and outputs, what it returns.

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

Prop

Type

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

runEstimatedCredits, runPerMinuteCredits and runPerItemCredits price the app run alone: the listed price without its Render final part, which runs and is charged on its own. editPlanPerMinute is true when an Edit Plan node is charged per started minute of the recording, and false when it is charged in steps of 15, 30, 60, 90, 120 or 180 minutes.

apps.run(slug, inputs?, opts?)

Runs an app and returns at once with { executionId, status, runId? }. Poll client.executions.get(executionId) for the result.

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

Prop

Type

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" } } },
)

An app that takes a video link, through an exposed Video URL node, is run with the link as the input value, for example client.apps.run("clip-pack", { episode: "https://www.youtube.com/watch?v=..." }). The server downloads a post link before the first node runs. A YouTube video of 4 minutes or longer is downloaded only as a part that you name with inputOverrides. Any other web link is used as it is.

inputOverrides is an advanced option. It can set, for example, a node's pre and post text (promptPrefix and promptSuffix). It cannot set a destination of an output node, such as a Webhook Output URL, a publishing account or a scraper's target. Such an override is refused with 400 locked_field.

apps.listRuns(slug, params?)

Lists your runs of an app, a page at a time.

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

Prop

Type

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

An AppRun has id, appSlug, executionId, status (pending, running, completed, failed or cancelled), inputs, outputs (each { nodeId, type, url?, text? }), startedAt and finishedAt.

apps.getRun(slug, runId)

Reads one run of an app.

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

Prop

Type

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

apps.resume(slug, runId, opts?)

Retries a failed run from where it stopped. This is the Retry button on the app's page. Only the steps the run did not complete run, and only those are charged. Every step it completed hands on its result. A UGC Clip that failed renders only the clips it did not finish.

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>

Prop

Type

  • The run's own version and inputs. The Retry runs the app version the run ran, with the inputs the run applied. It takes no inputs of its own.
  • The dry run. With dryRun: true, the answer is an AppRunResumePlan: runId, continuesExecutionId, appVersion, nodeIds, steps ({ nodeId, type, label }, the steps that run again), estimatedCredits, reusedClips, sufficient and available. estimatedCredits includes the creator's fee on the app, and is null without credits. sufficient says whether your balance and your app credits cover the Retry. appCreditsAllowance is present only when your app credits are what stops it. Show the plan before a Retry.
  • The started Retry. Without dryRun, the answer is { executionId, runId, status: "pending", nodeIds, deduped? }. Poll it with client.executions.get(executionId). The run's execution moves to the Retry's, so getRun then shows it.
  • One Retry at a time. A second call while that Retry is pending throws an AlreadyRunningError that names its execution. Only two presses at the same moment reach the same Retry, answered with deduped: true.

Refusals come before anything is written or billed. The code is one of AppRunResumeRefusalCode:

codeThrown asWhen
not_foundNotFoundErrorNo such run of yours under this app.
continuation_not_foundNotFoundErrorThe run's execution is not yours.
validation_errorNodaroError (400)A malformed run id or body, or the run of a component.
app_run_version_unavailableNodaroError (409)The version the run ran is gone. Start the run over.
app_run_version_changedAppRunVersionChangedErrorThe app is on another version than the run ran. Start over, or send version: err.runVersion.
continuation_workflow_mismatch / continuation_version_mismatchNodaroError (400)The run's execution ran another workflow, or another version of it. Start the run over.
continuation_not_failedNodaroError (409)The run completed or was stopped. Start it over.
already_runningAlreadyRunningError (409, executionId)The run is still running, or a Retry of it is pending. Follow executionId.
continuation_subset_requiredNodaroError (409, or 400)Nothing is left to retry (409), or the app runs no step (400).
locked_fieldNodaroError (400)An input the run applied sets a field the app does not let a runner set.
preview_review_requiredNodaroError (400)The Retry would stop at a Preview with nobody to review it. Retry it from the app's page.
insufficient_app_credits / insufficient_creditsInsufficientCreditsErrorYour app credits or your balance cannot cover the Retry. Its code says which.
billing_unavailableNodaroError (503)Billing is briefly unavailable for a workspace run. Try again shortly.

To run with other inputs or on the app's current version, start the run over with apps.run instead. Every step then runs, and is charged, again. See Retry a failed run.

import { AppRunVersionChangedError } from "@nodaro/sdk/errors"

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)

Archives a run. Restoring a run and deleting it permanently are only available in the Nodaro app, so a script cannot destroy data.

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

Prop

Type

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

client.templates

The template marketplace, public by design: browse it, read a template with its whole workflow, and clone it into your project for free.

templates.browse(params?)

Browses the marketplace, a page at a time (GET /v1/templates/browse). No sign-in is needed.

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

Prop

Type

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

On a deployment that does not allow public work (outputs.allowPublic: false), the marketplace lists nothing: { data: [], nextCursor: null }.

A card's estimatedCredits is the template's listed price. When the price follows the length of the recording the template is given, estimatedPerMinuteCredits is above 0. The price is then estimatedCredits plus that many credits for each minute of the recording. Otherwise estimatedPerMinuteCredits is absent or 0. When a choice in the template can cost more, the card also carries maxEstimatedCredits and maxEstimatedPerMinuteCredits, the top of the price range.

The route also takes sort=cheapest, the Fewest credits order of the Templates page. It sorts by the price of a 60-minute recording, lowest first: estimatedCredits plus 60 times estimatedPerMinuteCredits. The SDK's TemplateSort type does not include cheapest, so send it with client.request().

templates.get(slug)

Reads one public template with its full workflow snapshot: snapshotNodes, snapshotEdges and snapshotSettings (GET /v1/templates/:slug).

get(slug: string): Promise<Template>

Prop

Type

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

Throws NotFoundError when the slug is unknown, unlisted or inactive. On a deployment that does not allow public work, it also throws NotFoundError for someone else's template listed only on the marketplace. A template in the tutorial channel still answers.

templates.clone(slug, params)

Copies a template into one of your projects as a new workflow (POST /v1/templates/:slug/clone). It costs no credits.

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

Prop

Type

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

On a deployment that does not allow public work, someone else's marketplace template answers 404 not_found. Your own templates and tutorial templates clone as before.

client.tutorials

tutorials.list()

Lists every tutorial category with its tutorial videos and tutorial workflows (GET /v1/tutorials). It is public and read-only.

list(): Promise<{ categories: TutorialCategory[] }>
const { categories } = await client.tutorials.list()
for (const category of categories) {
  console.log(category.name, category.videos.length, category.flows.length)
}

Each category has id, name, slug, sortOrder, videos and flows. A video has a title, a videoUrl and a thumbnailUrl. A flow is a template marked as a tutorial: it has a title, a complexity, estimatedCredits, the node types and models it uses, and a slug you can pass to templates.get() and templates.clone().

Frequently asked questions

How do I run a Nodaro app from code?

Call client.apps.run with the app's slug and an inputs object whose keys match the app's input fields. It returns an executionId; poll client.executions.get(executionId) for the result.

How do I find the inputs an app expects?

Call client.apps.get(slug). Its inputSchema lists the fields end users fill in, and outputs lists what the app returns.

Does cloning a template cost credits?

No. client.templates.clone copies the template into one of your projects for free. You pay only when you run the new workflow.

How do I retry a failed app run from code?

Call client.apps.resume(slug, runId, { dryRun: true }) to see the steps and the price, then call it again without dryRun. Only the steps the run did not complete run, and only those are charged.

Can inputOverrides change where an app sends its results?

No. An override cannot set a destination, such as a Webhook Output URL or a publishing account. The run is refused with 400 locked_field.

Last updated on

On this page