Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
TypeScript SDK

Objects and creatures

Create objects and creatures, generate main images and variants, animate them, and make a creature talk from TypeScript with the Nodaro SDK.

client.objects scripts everything Object Studio does for props, products and vehicles, and client.creatures does the same for animals and creatures. Both create and edit items, generate main-image candidates, approve one, add variants and motion clips, and write a caption that keeps the item consistent in later prompts. The methods call the Objects and Creatures REST APIs. See Objects and Creatures for the editor view.

Methods

The two resources have the same methods.

MethodWhat it does
objects.list(params?), creatures.list(params?)List your items
objects.listArchived(params?)List your archived items
objects.get(id)Read one item, with its jobs in progress
objects.create(input), creatures.create(input)Create an item
objects.update(id, input), creatures.update(id, input)Change an item
objects.delete(id) and restore(id)Archive an item, or bring it back
objects.permanentDelete(id)Destroy an archived item and its files
objects.generate(input), creatures.generate(input)Generate main-image candidates
objects.generateAsset(input), creatures.generateAsset(input)Generate a variant
objects.generateMotion(input), creatures.generateMotion(input)Animate the main image into a clip
objects.approveMainImage(id, candidateJobId, expectedUpdatedAt?)Make a candidate the main image
objects.recaption(id)Write the description again

client.objects

An object has its main image in sourceImageUrl, four collections (angles, materials, variations and motionClips, each a list of { name, url }), boards, referencePhotos, canonicalDescription and styleLock. category is one of furniture, vehicle, weapon, food, clothing, electronics, nature, tool, animal or other.

Object shares its name with the JavaScript global. When you need both, import it under another name: import type { Object as NodaroObject } from "@nodaro/sdk".

objects.list(params?)

Lists your objects. By default it returns active objects only. Paging is optional: without limit you get the whole list; with limit, at most 500, you get one page and a nextCursor.

list(params?: { archived?: boolean; projectId?: string; limit?: number; cursor?: string }): Promise<{
  objects: Object[]
  nextCursor?: string | null
}>

Prop

Type

const { objects } = await client.objects.list()
const page = await client.objects.list({ limit: 100 })

objects.listArchived(params?)

Lists your archived objects, as a shortcut for list({ archived: true }). creatures.listArchived() works the same way.

listArchived(params?: { projectId?: string; limit?: number; cursor?: string }): Promise<{ objects: Object[]; nextCursor?: string | null }>

Prop

Type

const { objects: archived } = await client.objects.listArchived()

objects.get(id)

Reads one object, with pendingJobs, the variants still generating. An archived object is not returned: it throws NotFoundError. creatures.get() works the same way.

get(id: string): Promise<ObjectDetail>

Prop

Type

const object = await client.objects.get(objectId)
console.log(object.sourceImageUrl, object.materials)

objects.create(input)

Creates an object. name and nodeId are required. A script without a canvas node can pass "mcp-managed" as nodeId.

create(input: CreateObjectInput): Promise<{ id: string }>

Prop

Type

const { id: objectId } = await client.objects.create({
  nodeId: "mcp-managed",
  name: "Antique Lantern",
  description: "Weathered brass lantern with hand-engraved filigree",
  category: "tool",
  style: "realistic",
})

objects.update(id, input)

Changes an object. Only the fields you send are written. The variant collections are not part of this call, because generation jobs add to them while you work.

update(id: string, input: UpdateObjectInput): Promise<{ id: string; updatedAt: string }>

Prop

Type

await client.objects.update(objectId, {
  canonicalDescription: "A weathered brass lantern with engraved filigree and a glass chimney",
  expectedUpdatedAt: object.updatedAt,
})

The 409 concurrent_modification arrives as a plain NodaroError. Read the object again, merge, and retry.

objects.delete(id) and restore(id)

delete() archives an object. Repeating it on an archived object changes nothing. restore() brings it back; when the name now matches an active object's name, ignoring case, the server adds (restored) and returns the name it used.

delete(id: string): Promise<{ success: true; archived: true }>
restore(id: string): Promise<{ id: string; name: string }>

Prop

Type

await client.objects.delete(objectId)
const { name } = await client.objects.restore(objectId)

objects.permanentDelete(id)

Destroys an archived object and every stored file it references. It works on archived objects only: an active object fails with 400 not_archived. Archive first with delete(). creatures.permanentDelete() works the same way.

permanentDelete(id: string): Promise<{ success: true; permanent: true }>

Prop

Type

await client.objects.delete(objectId)
await client.objects.permanentDelete(objectId)

Nodaro's MCP tools do not offer this operation, so an AI assistant cannot destroy your objects.

objects.generate(input)

Generates main-image candidates (POST /v1/generate-object). With count above 1, every job is reserved before any starts, so a failure part way through rolls the batch back.

generate(input: GenerateObjectInput): Promise<{ jobIds: string[]; jobId?: string }>

Prop

Type

const { jobIds } = await client.objects.generate({ name: "Antique Lantern", count: 4 })
for (const jobId of jobIds) {
  // poll each candidate with client.jobs.getStatus(jobId)
}

jobIds is always present, with one id per candidate. jobId is an older alias for a single candidate; use jobIds. With attachToObjectId and one candidate, the result becomes the main image when the job completes. Otherwise, pick one with approveMainImage().

objects.generateAsset(input)

Generates one variant (POST /v1/generate-object-asset). With attachToObjectId, attachToColumn and attachName, the result is added to that collection when the job completes.

generateAsset(input: GenerateObjectAssetInput): Promise<{ jobId: string }>

Prop

Type

const { jobId } = await client.objects.generateAsset({
  name: "Antique Lantern",
  assetType: "materials",
  variant: "gold",
  attachToObjectId: objectId,
  attachToColumn: "materials",
  attachName: "gold",
})

For angles, materials, variations and motion, the collection follows from the asset type. A custom variant needs attachToColumn.

objects.generateMotion(input)

Animates the object's image into a clip (POST /v1/generate-object-motion), with Generate Video in image-to-video mode. The clip always goes to motionClips. The defaults suit product shots: the model is kling-turbo and the frame is 1:1.

generateMotion(input: GenerateObjectMotionInput): Promise<{ jobId: string }>

Prop

Type

const { jobId } = await client.objects.generateMotion({
  name: "Antique Lantern",
  motionPrompt: "Slow 360-degree rotation, soft golden rim light",
  sourceImageUrl: object.sourceImageUrl!,
  attachToObjectId: objectId,
  attachName: "rotate-360",
})

objects.approveMainImage(id, candidateJobId, expectedUpdatedAt?)

Makes a completed candidate from generate() the object's main image. A vision model then writes the object's description, and the method returns both.

approveMainImage(id: string, candidateJobId: string, expectedUpdatedAt?: string): Promise<{
  sourceImageUrl: string
  canonicalDescription: string | null
}>

Prop

Type

const { sourceImageUrl, canonicalDescription } = await client.objects.approveMainImage(objectId, jobIds[0])

canonicalDescription is null when the description could not be written. The main image is still set; call recaption() to try again.

objects.recaption(id)

Writes the object's description again from its current main image. The call is safe to repeat and takes no concurrency token.

recaption(id: string): Promise<{ canonicalDescription: string }>

Prop

Type

const { canonicalDescription } = await client.objects.recaption(objectId)

Fails with 400 main_image_required when the object has no main image, and with a 502 when the vision model fails.

client.creatures

A creature is an animal or a fantasy being. It works like an object, with four differences:

  • species is a free-text type, such as dragon or wolf, and it is the subject of the main-image prompt. category is free text too.
  • poses replaces materials, so the collections are angles, poses, variations and motionClips. The variant types are angles, poses, variations and custom.
  • boards holds up to 24 named Creature Boards, dense reference sheets made with the Creature Board preset of Generate Image. You own this list: create() and update() replace it as a whole.
  • voice makes the creature a talking creature. It has the same shape as a character's voice: { voiceId, voiceName, traits, voiceType?, previewUrl?, ttsProvider? }. Pass voice: null to remove it.

creatures.listArchived(), get(), delete(), restore(), permanentDelete(), approveMainImage() and recaption() take the same arguments and behave like the object methods above.

creatures.list(params?)

list(params?: { archived?: boolean; projectId?: string; limit?: number; cursor?: string }): Promise<{
  creatures: Creature[]
  nextCursor?: string | null
}>

Prop

Type

const { creatures } = await client.creatures.list()

creatures.create(input)

create(input: CreateCreatureInput): Promise<{ id: string }>

Prop

Type

const { id: creatureId } = await client.creatures.create({
  nodeId: "mcp-managed",
  name: "Biscuit",
  species: "ginger cat",
  style: "realistic",
})

creatures.update(id, input)

Takes the fields of create() except nodeId, plus boards, selectedAssetByVariant and expectedUpdatedAt. Only the fields you send are written.

update(id: string, input: UpdateCreatureInput): Promise<{ id: string; updatedAt: string }>

Prop

Type

await client.creatures.update(creatureId, {
  voice: { voiceId: chosenVoiceId, voiceName: "Aria", traits: "smug, unhurried" },
})

creatures.generate(input)

Generates main-image candidates. It takes the fields of objects.generate(), plus species, and returns { jobIds }.

generate(input: GenerateCreatureInput): Promise<{ jobIds: string[]; jobId?: string }>

Prop

Type

const { jobIds } = await client.creatures.generate({ name: "Biscuit", species: "ginger cat", count: 4 })

creatures.generateAsset(input)

Generates one variant. It works like objects.generateAsset(), with attachToCreatureId.

generateAsset(input: GenerateCreatureAssetInput): Promise<{ jobId: string }>

Prop

Type

await client.creatures.generateAsset({
  name: "Biscuit",
  assetType: "poses",
  variant: "sitting",
  attachToCreatureId: creatureId,
  attachToColumn: "poses",
  attachName: "sitting",
})

creatures.generateMotion(input)

Animates the creature's image into a clip. It works like objects.generateMotion(), with the same defaults, kling-turbo and 1:1, and adds the clip to motionClips.

generateMotion(input: GenerateCreatureMotionInput): Promise<{ jobId: string }>

Prop

Type

await client.creatures.generateMotion({
  name: "Biscuit",
  motionPrompt: "The cat stretches, then yawns",
  sourceImageUrl: creature.sourceImageUrl!,
  attachToCreatureId: creatureId,
})

Make a creature talk

Speech needs no creature-specific method. Render the line with the creature's voice, then lip-sync it onto the creature's image:

const creature = await client.creatures.get(creatureId)

// 1. Speak the line in the creature's voice
const speech = await client.nodes.runAndWait("text-to-speech", {
  text: "I knocked the vase off the shelf. I regret nothing.",
  voice: creature.voice!.voiceId,
  provider: creature.voice!.ttsProvider,
  voiceType: creature.voice!.voiceType,
})

// 2. Lip-sync the audio onto the creature's main image
const clip = await client.nodes.runAndWait("lip-sync", {
  imageUrl: creature.sourceImageUrl!,
  audioUrl: speech.audioUrl,
  provider: "kling-avatar",
})
console.log(clip.videoUrl)

The Lip Sync node also dubs an existing video: pass videoUrl and a model that takes video. volcengine-lipsync is the lowest-priced option for dubbing and the only one that handles several speakers:

const dub = await client.nodes.runAndWait("lip-sync", {
  videoUrl: "https://example.com/scene.mp4",
  audioUrl: "https://example.com/new-vocal.mp3",
  provider: "volcengine-lipsync",
  mode: "basic",          // for complex scenes
  openScenedet: true,     // several speakers: scene and speaker detection
  audioDurationSec: 42,   // sets the per-second price; without it, you pay for 5 minutes
})

To bind a creature into a shot as a reference, build the reference with toConnectedReference({ kind: "creature", id, name, url, description }) from @nodaro/shared. Generate Image then adds a line that keeps the creature's anatomy, markings and colors. See References.

Frequently asked questions

Last updated on

On this page