Location Platform

Nodaro’s location platform lets you script every Location Studio operation through REST, the typed SDK, the CLI, and MCP. A location is a canonical environment row — name, establishing shot, identity copy, plus six asset buckets (timeOfDay, weather, seasons, angles, lighting, atmosphereMotions) — that downstream generation nodes reference to keep the same setting looking like the same place across every shot in a production.

This guide explains the data model, the four surfaces, and the canonical “create → main image → approve → layer variants → animate atmosphere” flow.

When to use which surface

Surface Reach for it when… Lives at
REST curl-able, language-agnostic, simplest /v1/locations*, /v1/generate-location*
SDK (@nodaro/sdk) Building a typed integration in Node / browser / Bun / Deno client.locations.*
CLI (nodaro / @nodaro/cli) Terminal scripts, cron, CI, ad-hoc one-shots nodaro locations …
MCP An LLM agent (Claude.ai, Cursor, etc.) is driving the work create_location, approve_main_image, etc.

All four surfaces share the same database row and the same Worker pipeline; they’re four ways to call the same routes.

The location row

The locations table stores one row per location. Highlights:

Column Type Purpose
id uuid Stable identifier.
user_id uuid Owner. Every query is scoped by this.
node_id text Canvas node id the row was first bound to. MCP-created rows use the "mcp-managed" sentinel.
project_id uuid (nullable) Project the row belongs to. Nullable for MCP-created rows.
workflow_id uuid (nullable) Workflow the row was first bound to.
name text Display name.
description text Freeform identity notes.
category text One of indoor, outdoor, urban, nature, fantasy, sci-fi, historical, futuristic, other.
style text Visual style — one of realistic, anime, 3d-pixar, illustration.
source_image_url text The anchor establishing shot — set by approve-main-image.
image_provider text MODEL_CATALOG image-model id the main image was generated with (nullable). Set on create + editable via the update route; validated server-side — unknown / non-image → null.
canonical_description text LLM-authored ~80–120-word visual description set when the main image is approved. Coerced from DB null to "" on the wire.
style_lock boolean When true, every variant gen passes the main image as i2i source for layout consistency. Defaults to true on new rows.
time_of_day / weather / seasons / angles / lighting jsonb[] Five image asset buckets — each entry is { name, url }.
atmosphere_motions jsonb[] The sixth bucket: looping video clips animated from the main image (i2v). Each entry is { name, url } where url is a video.
reference_photos jsonb[] Mood-board photos (cap 20), each { kind, url } with kind ∈ {wide, interior, exterior, detail, moodBoard, other}.
selected_asset_by_variant jsonb The user’s chosen DEFAULT take per variant (Studio version history). OPAQUE map: key "<bucket>:<variant>" (e.g. timeOfDay:dawn) → the chosen asset URL (one already in that bucket). Keys stored verbatim (not normalized); soft-capped at 200 keys / 2048-char values, overflow dropped silently. A separate column — a selection never rewrites an asset bucket.
deleted_at timestamptz Non-null = soft-deleted (archived).
created_at / updated_at timestamptz Timestamps.

Soft delete is the only delete the public programmatic surface exposes. The studio archive view + REST /restore route bring a row back; permanent destruction is reachable only via DELETE /v1/locations/:id?permanent=true which is UI-only by design — the SDK, CLI, and MCP surfaces all soft-delete.

The schema does not carry per-bucket status columns. In-flight asset generations are surfaced via pendingJobs on GET /v1/locations/:id, derived at request time from rows in the jobs table where input_data.attachToLocationId = :id and status ∈ {pending, running}.

Completed candidate main images are surfaced via previousCandidates on GET /v1/locations/:id (newest first, max 5): completed generate-location jobs tracked to the row via input_data.candidateForLocationId, excluding the one whose URL is the current source_image_url. This powers a “pick from N / keep the original” UI - the current main image is left untouched until the user approves one (see “The main-image approval flow” below).

Asset arrays explained

Six bucket columns hold the variants of a location’s anchor establishing shot. Each entry is { name, url }:

Bucket What it represents Example variant names
timeOfDay Same framing, different time of day dawn, morning, noon, afternoon, golden hour, dusk, blue hour, night, midnight
weather Same framing, different weather clear, cloudy, light rain, heavy rain, storm, snow, blizzard, fog, mist
seasons Same framing, seasonal swap spring, summer, autumn, winter
angles Camera-angle variants of the same place wide, medium, closeup, aerial, low-angle, eye-level, bird's-eye, dutch tilt
lighting Lighting-setup variants soft natural, harsh sunlight, golden, blue hour, neon, candlelit, cinematic, dramatic chiaroscuro
atmosphereMotions Looping video clips animating the main image (i2v) slow dolly-in, slow pan-left, drone fly-over, gentle drift, parallax

Each variant is generated independently via POST /v1/generate-location-asset (or POST /v1/generate-location-motion for atmosphereMotions). The result is appended to the named bucket on completion when attachToLocationId + attachToColumn + attachName are supplied.

The worker uses the append_location_asset(p_location_id, p_column, p_value) Postgres RPC for the append — it’s atomic per-column with a URL dedup guard and a deleted_at IS NULL predicate, so two concurrent jobs can’t clobber each other and a job finishing after a soft-delete won’t resurrect the row.

Seamless 360° surround continuation

Cloud edition only. The route is registered on every edition, but Community/Business requests get an immediate 403 edition_required before any processing happens.

POST /v1/generate-surround-continuation builds a look-around (45°, 90°, … ring views) one frame at a time. Each call generates the next ring view as an image-to-image continuation of the previous one (referenceImageUrl), so a panorama viewer can stitch the views edge-to-edge.

The platform owns the whole pipeline:

  1. Half-carry composite — it carries the reference’s trailing half into the new frame’s leading half (per direction) and grays the rest; the model paints only the gray region.
  2. Paint — image-to-image off the composite (studio pins provider: "nano-banana-pro", aspectRatio: "16:9").
  3. Color harmonization — it matches the painted half’s exposure / white balance / color grade to the carried half and feathers the seam, so there is no tonal seam down the frame’s center even when the model drifts warm. The carried half stays pixel-exact, so adjacent ring views line up perfectly.

direction is right (turn right), up (tilt up), or down (tilt down); carriedFraction defaults to 0.5. The result attaches to the location’s angles bucket when attachToLocationId + attachToColumn: "angles" + attachName are supplied. Billed per image provider — same per-provider table as generate-location-asset.

Atmosphere motion clips

POST /v1/generate-location-motion animates the location’s establishing shot into an ambient camera-move clip — drifting fog, slow camera dollies, parallax pulls, drone fly-overs — for use as start_frame references in downstream i2v nodes or as B-roll loops.

Refinement (video-to-video)

Pass refineFromVideoUrl (REST) / refine_from_video_url (MCP) to route the worker through video-to-video using THAT clip as the source instead of running image-to-video from the source frame. Use to iterate on an existing atmosphere clip with a new prompt without shifting composition:

# REST — refine an existing fog clip into rain
curl -X POST $API/v1/generate-location-motion \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -d '{
    "motionPrompt": "same shot but light rain instead of fog",
    "sourceImageUrl": "https://r2/loc.png",
    "refineFromVideoUrl": "https://r2/loc-fog.mp4",
    "provider": "wan-i2v",
    "name": "Old Library"
  }'

Routes through providers with the video-to-video capability (currently Wan 2.6 via KIE). Same auto-attach behavior — the refined clip lands in atmosphere_motions[] when attachToLocationId is set.

Reference photos (mood-board)

The mood-board is a small array of caller-supplied reference images that travel with the location and become additional reference inputs for any downstream node that references the location via FieldMappings — even without a wired edge. Each entry is { kind, url }:

kind What it’s for
wide A wider-establishing crop of the same place.
interior Interior shot when the main image is an exterior (or vice versa).
exterior The reverse.
detail Close-up of a defining detail (statue, sign, plant species, material).
moodBoard Vibe / palette / aesthetic reference.
other Free-form bucket.

Caps:

Pass the array via referencePhotos on the create / update body. The canonical-fallback injector picks the entries up automatically whenever a downstream consumer references the location, and per-consumer suppression is available via the canvas’s Injected References × button.

Kind-tagged conditioning

Each reference photo’s kind propagates into the prompt builder’s subject line — Image 1 (Old Library — wide-angle reference) — <canonical description> — so the model understands the role of each ref at generate time (wide-angle establishing context vs. interior detail vs. mood-board inspiration). The kind labels are stable; you don’t need to change anything in your call sites.

Reference photos may contain people’s faces. Before the first photo can be added to a location (via the studio UI), the user must tick a consent checkbox confirming they have rights and consent. The tick is captured as a timestamp on the locations.pii_consent_at column.

The main-image approval flow

Generating an establishing shot is a three-step pipeline:

  1. GeneratePOST /v1/generate-location produces 1–10 candidate jobs (API accepts 1–10; common UI presets are 1, 2, or 4). With attachToLocationId set AND count === 1, the worker writes the result to source_image_url for the single job (auto-approve for single-candidate runs).
  2. Approve — for multi-candidate runs, POST /v1/locations/:id/approve-main-image with the chosen candidateJobId sets source_image_url AND fires an LLM caption (Claude Sonnet vision) inline to populate canonical_description.
  3. Caption — if the caption sub-failed during approval, canonicalDescription comes back as "" (not null). Retry via POST /v1/locations/:id/llm-caption (502s on LLM failure; 400 no_source_image when no main image is set). Both routes are idempotent and safe to re-run.

canonical_description is what downstream prompts inject when they reference this location (“A neon-soaked Tokyo alley after midnight, with mismatched vending machines lining a wet concrete corridor…”). Without it, visual drift between scenes is much more likely.

Candidate surfacing + non-destructive refine

For count > 1 runs no candidate auto-attaches. Every candidate job is tracked to the location via input_data.candidateForLocationId (written for count === 1 AND count > 1, and never read by any auto-attach path), so completed candidates surface on GET /v1/locations/:id as previousCandidates - newest first, max 5, excluding the one whose URL is the current source_image_url. The studio renders these as a “pick from N” strip and promotes a winner via approve-main-image; the current main image stays intact until the user picks, so refining is non-destructive.

POST /v1/generate-location also honors a transient userPrompt edit instruction. When present it drives the generation prompt directly - paired with an i2i sourceImageUrl the source establishing shot is edited toward the instruction. It is never persisted, so the stored name, description, and canonical_description are left untouched (the route only image-only auto-attaches). Omit userPrompt and the prompt is built from the persisted scene fields exactly as before.

Using location assets as references

After the assets are populated, downstream generation calls reference the URLs directly. Two patterns:

Pattern A — explicit reference URLs. Most generation nodes accept reference_images (or referenceImages in the SDK). Pass any combination of asset URLs to anchor the new image to the location:

const location = await client.locations.get(locationId)
const stormUrl = location.weather?.find(w => w.name === "storm")?.url

await client.nodes.run("generate-image", {
  prompt: "the same alley under a thunderstorm, a courier sprinting through",
  reference_images: [stormUrl].filter(Boolean),
})

Pattern B — {locationName} field-mapping in editor prompts. Inside the canvas, prompt fields support {Rainy Tokyo Alley} interpolation that resolves at execution time to the location’s canonical description + the main image attached as a reference (canonical fallback). Style Lock controls whether the canonical description is injected verbatim or as soft guidance.

For programmatic flows, prefer Pattern A — explicit URLs are easier to reason about and don’t depend on the canvas wiring.

Pattern C — @location:N:bucket/variant mention syntax (canvas). In generate-image / image-to-image / modify-image prompts, type @oldlibrary:1:weather/rain to pin a specific variant inline. The slug is the location’s slugified name (old-libraryoldlibrary — see locationMentionSlug in @nodaro/shared); the bucket/variant segment maps to one of the 6 asset buckets (timeOfDay, weather, seasons, angles, lighting, atmosphereMotions) and the variant’s slugified name. Three optional shapes:

Shape Effect
@oldlibrary:1 Canonical reference image, identical mode
@oldlibrary:1:layout Canonical with style / layout / none mode override
@oldlibrary:1:weather/rain Pin the rain variant (bucket/variant pair)
@oldlibrary:1:weather/rain:style Variant + mode override

The 4 usage modes (identical, style, layout, none) control how the model uses the reference — match exactly, style/mood transfer, compositional layout transfer, or attach the image without textual bias. The studio’s autocomplete pill (cyan) shows the mode via a dropdown.

Pattern D — Smart variant selection (automatic). When a wired location feeds a generator and you DON’T type a @location:N:variant mention, the prompt-builder scans your prompt for keywords matching the location’s variant names. "at sunset"timeOfDay/dusk if you have a dusk variant; "rainy evening"weather/rain; "neon-lit street"lighting/neon. A small synonym table handles common alternatives (“sunset” matches “dusk”, “rainy” matches “rain”). Bucket priority on ties: timeOfDay > weather > seasons > lighting > angles > atmosphereMotions. Explicit @-mention always wins over smart match.

Style Lock semantics

Style Lock is the location platform’s most important consistency switch. It’s a single boolean (styleLock) defaulting to true on new rows that travels with the location row and is read at every gen-time decision:

Toggle Style Lock via:

Quickstart by surface

REST

TOKEN="ndr_..."
BASE="https://nodaro.example.com"

# Create
LOC=$(curl -s -X POST "$BASE/v1/locations" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "nodeId": "scripted",
    "name": "Rainy Tokyo Alley",
    "description": "Neon-soaked alley with vending machines and wet pavement",
    "category": "urban",
    "style": "realistic"
  }' | jq -r .id)

# Generate one establishing shot (auto-attaches)
curl -s -X POST "$BASE/v1/generate-location" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"Rainy Tokyo Alley\",\"count\":1,\"attachToLocationId\":\"$LOC\"}"

# (after job completes) Re-fetch the row
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/locations/$LOC" | jq .sourceImageUrl

# Generate a weather variant
curl -s -X POST "$BASE/v1/generate-location-asset" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"assetType\": \"weather\",
    \"variant\": \"storm\",
    \"name\": \"Rainy Tokyo Alley\",
    \"attachToLocationId\": \"$LOC\",
    \"attachToColumn\": \"weather\",
    \"attachName\": \"storm\"
  }"

# Animate the establishing shot into an atmospheric motion clip
MAIN=$(curl -s -H "Authorization: Bearer $TOKEN" "$BASE/v1/locations/$LOC" | jq -r .sourceImageUrl)
curl -s -X POST "$BASE/v1/generate-location-motion" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"Rainy Tokyo Alley\",
    \"motionPrompt\": \"slow dolly-in, neon signs flicker, light rain falling\",
    \"sourceImageUrl\": \"$MAIN\",
    \"provider\": \"kling\",
    \"attachToLocationId\": \"$LOC\",
    \"attachName\": \"neon dolly-in\"
  }"

Endpoint reference

Method Path Auth Purpose
GET /v1/locations JWT / ndr_ List active. Pass ?archived=true to list archived.
GET /v1/locations/:id JWT / ndr_ Detail row + pendingJobs[] + previousCandidates[] (completed candidate main images, max 5). Soft-deleted rows ARE returned by id.
POST /v1/locations JWT / ndr_ Upsert. With id → UPDATE (worker-owned columns dropped); without → INSERT. Optimistic-concurrency via expectedUpdatedAt.
DELETE /v1/locations/:id JWT / ndr_ Soft-delete (sets deleted_at).
DELETE /v1/locations/:id?permanent=true JWT / ndr_ Permanent destroy. Row must already be archived (400 not_archived otherwise). UI-only by design — SDK, CLI, MCP all omit this path.
POST /v1/locations/:id/restore JWT / ndr_ Un-archive. Auto-suffixes "(restored)" on name collision.
POST /v1/locations/:id/approve-main-image JWT / ndr_ Approve a candidate; sets source_image_url, fires LLM caption inline.
POST /v1/locations/:id/llm-caption JWT / ndr_ Re-run LLM caption against the current main image.
POST /v1/generate-location JWT / ndr_ Generate 1–10 candidate establishing shots. Optional transient userPrompt drives an i2i edit (never written to the row).
POST /v1/generate-location-asset JWT / ndr_ Generate one variant for any of the 5 image buckets (or custom).
POST /v1/generate-surround-continuation JWT / ndr_ (Cloud only) Generate the next seamless 360° ring view (half-carry composite + paint + color-harmonize); attaches to angles.
POST /v1/generate-location-motion JWT / ndr_ Animate the establishing shot into an atmosphere motion clip via i2v.

All /generate-* routes return a jobId (count=1) or jobIds[] (count=2/4) and reserve credits up-front before any worker job is enqueued.

SDK

import { createClient, StaticTokenAuth } from "@nodaro/sdk"

const client = createClient({
  baseUrl: "https://nodaro.example.com",
  auth: new StaticTokenAuth(process.env.NODARO_TOKEN!),
})

// Create a location, generate one main image, auto-attach on completion.
const { id: locationId } = await client.locations.create({
  nodeId: "scripted",
  name: "Rainy Tokyo Alley",
  description: "Neon-soaked alley with vending machines and wet pavement",
  category: "urban",
  style: "realistic",
})

const generated = await client.locations.generate({
  name: "Rainy Tokyo Alley",
  count: 1,
  attachToLocationId: locationId,
})
// Always returns `{ jobIds: string[] }` (`jobId` is a deprecated alias for single-candidate runs).

// Poll the job; with count=1 the worker auto-attaches the result on completion.

// For multi-candidate runs, explicitly approve the chosen candidate:
const approved = await client.locations.approveMainImage(
  locationId,
  "<candidateJobId>",
)
// approved.sourceImageUrl + approved.canonicalDescription

// Generate a weather variant — auto-attaches on completion.
await client.locations.generateAsset({
  assetType: "weather",
  variant: "storm",
  name: "Rainy Tokyo Alley",
  attachToLocationId: locationId,
  attachToColumn: "weather",
  attachName: "storm",
})

// Generate the next 360° ring view — the platform builds the half-carry
// composite, paints, and color-harmonizes so there is no seam; the carried half
// stays pixel-exact and the result auto-attaches to the `angles` bucket.
await client.locations.generateSurroundContinuation({
  referenceImageUrl: previousRingView,
  direction: "right",
  degrees: 45,
  provider: "nano-banana-pro",
  aspectRatio: "16:9",
  attachToLocationId: locationId,
  attachToColumn: "angles",
  attachName: "Surround 45°",
})

// Animate the establishing shot into an atmosphere motion clip.
await client.locations.generateMotion({
  motionPrompt: "slow dolly-in, neon signs flicker, light rain falling",
  sourceImageUrl: approved.sourceImageUrl,
  provider: "kling",
  name: "Rainy Tokyo Alley",
  attachToLocationId: locationId,
  attachName: "neon dolly-in",
  // aspectRatio defaults to "16:9" server-side; override here if needed.
})

// Flip Style Lock off for a one-off mash-up gen.
await client.locations.update(locationId, { styleLock: false })

// Soft-delete (archive) — recoverable.
await client.locations.delete(locationId)

// List the archive.
const { locations } = await client.locations.list({ archived: true })

// Restore from the archive.
await client.locations.restore(locationId)

Full surface: list / get / create / update / delete (soft) / restore / generate / generateAsset / generateSurroundContinuation / generateMotion / approveMainImage / recaption. Permanent-delete is intentionally absent — see the Soft delete + archive section.

CLI

# Create a location
nodaro locations create "Rainy Tokyo Alley" \
  --node-id scripted \
  --description "Neon-soaked alley with vending machines" \
  --category urban --style realistic

# Generate one main image and auto-attach
nodaro locations generate \
  --name "Rainy Tokyo Alley" \
  --count 1 \
  --attach-to-location-id <location-id> \
  --watch

# Generate a single environmental variant
nodaro locations generate-asset <location-id> \
  --asset-type weather \
  --variant storm \
  --watch

# Animate the establishing shot into an atmospheric motion clip
nodaro locations generate-motion \
  --name "Rainy Tokyo Alley" \
  --motion-prompt "slow dolly-in, neon signs flicker, light rain falling" \
  --source-image-url "https://r2.example/locations/tokyo-alley-main.png" \
  --provider kling \
  --attach-to-location-id <location-id> \
  --attach-name "neon dolly-in" \
  --aspect-ratio 16:9 \
  --watch

# Approve a candidate as the main image (multi-candidate flow)
nodaro locations approve-main-image <location-id> \
  --candidate-job-id <job-id>

# Re-run the LLM caption against the current main image
nodaro locations recaption <location-id>

# Update Style Lock
nodaro locations update <location-id> --style-lock false

# Archive / list archived / restore
nodaro locations delete <location-id>
nodaro locations list --archived
nodaro locations restore <location-id>

Pass --json to any command for machine-readable output and --watch to commands that fire jobs (generate, generate-asset, generate-motion) to poll until completion. Multi-profile auth lives at ~/.config/nodaro/config.json; switch profiles with --profile.

MCP

Eight location tools are exposed, gated by scope. Two (generate_location, generate_location_motion) are verb-style entries that live in the shared verbs-* / locations.ts registry alongside generate_image and generate_character, while the other six (list, get, create, update, approve, recaption) live in the dedicated locations.ts MCP module.

Tool Scope What it does
list_locations assets:read Summary list (name, main image URL, asset counts, identity copy). Pass archived: true for the archive.
get_location assets:read Full detail including all six asset arrays + reference photos.
create_location assets:write Create a new row with name + optional description / category / style. Returns the new id.
update_location assets:write Update identity fields (name / description / category / style / styleLock / canonicalDescription). Optimistic-concurrency via expectedUpdatedAt.
approve_main_image assets:write Approve a completed generate_location candidate as the main image. Fires the LLM caption inline.
recaption_location assets:write Re-run the LLM caption against the current main image.
generate_location workflows:execute Generate a main image (kind: "main") or a variant asset (kind: "asset" + asset_type + variant).
generate_location_motion workflows:execute Animate the main image into an atmospheric motion clip via i2v. Hardcoded attach column = atmosphere_motions.

delete_location and restore_location are intentionally not exposed via MCP — destructive (or destructive-adjacent) operations driven by an LLM are dangerous, and even a soft delete is hard to undo without context the LLM doesn’t have. Users (and SDK / CLI integrations on their behalf) can still archive + restore through REST.

create_location({
  name: "Rainy Tokyo Alley",
  description: "Neon-soaked alley with vending machines",
  category: "urban",
  style: "realistic"
})
// → { id: "loc-uuid", name: "Rainy Tokyo Alley" }

// Main image (single candidate — auto-attaches on completion).
generate_location({
  kind: "main",
  name: "Rainy Tokyo Alley",
  attach_to_location_id: "loc-uuid"
})
// → { content: [text], structuredContent: { jobId: "job-1" } }

// Multi-candidate flow — approve the winner explicitly.
approve_main_image({
  location_id: "loc-uuid",
  candidate_job_id: "job-1"
})
// → { sourceImageUrl, canonicalDescription }

// Variant asset — auto-attaches to the `weather` bucket.
generate_location({
  kind: "asset",
  name: "Rainy Tokyo Alley",
  asset_type: "weather",
  variant: "storm",
  attach_to_location_id: "loc-uuid"
})

// Custom asset — caller must supply attach_to_column.
generate_location({
  kind: "asset",
  name: "Rainy Tokyo Alley",
  asset_type: "custom",
  variant: "neon-soaked midnight",
  attach_to_location_id: "loc-uuid",
  attach_to_column: "lighting",
  attach_name: "Neon Midnight"
})

// Atmosphere motion clip — animates the approved main image.
generate_location_motion({
  motion_prompt: "slow dolly-in, neon signs flicker, light rain falling",
  source_image_url: "https://r2.example/locations/tokyo-alley-main.png",
  provider: "kling",
  name: "Rainy Tokyo Alley",
  attach_to_location_id: "loc-uuid",
  attach_name: "neon dolly-in"
})

// Refinement — iterate an existing clip via video-to-video.
generate_location_motion({
  motion_prompt: "same shot but light rain instead of fog",
  source_image_url: "https://r2/loc.png",
  refine_from_video_url: "https://r2/loc-fog.mp4",
  provider: "wan-i2v",
  name: "Rainy Tokyo Alley"
})

App input parameterization for locations

When a workflow with a wired Location node is published as an app (/v1/apps), the location surfaces as an app input via get_app_inputs with fieldKey: "selectedVariant". Callers pass a slug-form string to pin a variant at run time:

run_app({
  slug: "neon-noir-poster",
  inputs: {
    my_location: "weather/rain"   // "<bucket>/<variant-name>"
  }
})

The orchestrator looks up the variant in the location’s asset buckets and patches sourceImageUrl so all downstream consumers see it as canonical for this run. Format is <bucket>/<variant-name> where bucket is one of the 6 asset buckets and variant-name slugifies to match the publisher- stored entry (case-insensitive, Light Rainlight-rain). Unknown buckets or unmatched variant names fall through to canonical silently.

Variant names for canonical asset types:

Asset type Preset variants
timeOfDay dawn, morning, noon, afternoon, golden hour, dusk, blue hour, night, midnight
weather clear, cloudy, light rain, heavy rain, storm, snow, blizzard, fog, mist
seasons spring, summer, autumn, winter
angles wide, medium, closeup, aerial, low-angle, eye-level, bird’s-eye, dutch tilt
lighting soft natural, harsh sunlight, golden, blue hour, neon, candlelit, cinematic, dramatic chiaroscuro
custom any short label — pair with attach_to_column when attaching to a location row
atmosphereMotions (via generate_location_motion) slow dolly-in, slow pan-left, slow pan-right, push up, drone fly-over, gentle drift, parallax, static atmospheric

See docs/mcp/tools.md for the full parameter reference.

Identity-foundation fields (advanced)

For high-fidelity location work, three fields work together to anchor the sense of place across many generations:

For everyday use, you can leave the mood-board empty and let the LLM caption do the work via canonicalDescription. For production-grade visual consistency on unfamiliar or fantastical locations, populate a 3–6 image mood-board up-front via create() / update() — the model has no prior to anchor against and the wider context translates directly into more faithful first-pass generations.

Soft delete + archive

DELETE /v1/locations/:id sets deleted_at rather than dropping the row. The location disappears from list() by default but remains loadable via get(id) so canvas nodes pointing at it keep rendering.

To see archived rows: list({ archived: true }) (REST: ?archived=true). To un-archive: POST /v1/locations/:id/restore. If the name now collides (case-insensitive) with another active row, the server auto-suffixes "(restored)" and returns the effective name.

Surface Delete (archive) Restore Permanent delete
REST DELETE /v1/locations/:id POST /v1/locations/:id/restore DELETE /v1/locations/:id?permanent=true (archived rows only)
SDK client.locations.delete(id) client.locations.restore(id) Not exposed
CLI nodaro locations delete <id> nodaro locations restore <id> Not exposed
MCP Not exposed Not exposed Not exposed

Permanent deletion is intentionally NOT exposed through the SDK, CLI, or MCP surfaces. The archive view in the editor (/library/locations) is the only path: a two-step typed-name confirmation flow that calls DELETE /v1/locations/:id?permanent=true. That route requires the row to already be archived (returns 400 not_archived otherwise) and reaps the referenced R2 keys (main image, the 6 asset buckets, reference photos) via batchDeleteFromR2. The MCP surface intentionally omits delete and restore entirely so an LLM cannot trigger them.

Pricing notes

Location generation is metered through the same credit pipeline as other generation routes:

Pricing is fetched from the model_pricing table at runtime; the static fallback in STATIC_CREDIT_COSTS lists "location": 2 as the default identifier when an image provider isn’t supplied. See the Architecture doc for the full credit-flow walkthrough.

See also