About the scripting engine

A script is plain JavaScript (ES2022, single file, strict mode, top-level await supported) that drives your camera over local Wi-Fi entirely on-device - no network, no accounts, no telemetry. Anything the app UI can do, a script can do, plus environment data the UI does not expose directly (sun/moon geometry, shadows, moon phase).

Scripts are sandboxed and security-hardened: no filesystem, no network, no host environment access. Scripts can only touch the documented namespaces in this reference - everything else is unavailable, not restricted. There is no Math.random (use rng), no host timers (use time.*), and no wall-clock reads outside time.*.

Lifecycle & stopping

A script run proceeds: saved → compiled → validated → estimated → running → finished | stopped (user) | killed (limit) | error. A running script is the session: session state, the trial cap, keep-awake, and the disconnect alarm all apply exactly as for the built-in intervalometer. A script run cannot be paused manually (run/stop only); the only automatic pause is the camera-loss pause policy state, where the in-flight camera call simply waits.

Pressing Stop requests a clean stop: the script's next step observes it and receives a ScriptStopped error. Stopping is safe - counters are preserved and any in-flight shot command is released.

Scripts have no shot-count or duration bounds by design: a script runs until it returns, you stop it, the trial cap is reached, or a session failure occurs. Multi-day time-lapses are a first-class use case. The editor's pre-run estimate runs a dry-run simulation and reports the full-run step count - it also sizes the internal step budget for you when you save (below).

Safety limits & budgets

Every call into the documented API counts as two steps (entry and completion); long-running host calls (shots, bulb holds, location acquisition) additionally count a step on each internal progress tick. The only free call is location.get() (a pure cache read). The engine counts steps and execution time so a buggy script can never freeze the app:

LimitDefaultHard maximumWhere configured
CPU time (cumulative busy)internal 24 h backstop none user-facingnot user-configurable
Steps per run10 000none set automatically by the editor when you save (from the step estimate)
Run duration (wall-clock)-none not limitable

If a script loops without calling the API for more than 500 ms of uninterrupted script execution, or exhausts its step budget or memory, it terminates with a readable ScriptLimitExceeded error in the script console - the session is not corrupted (counters preserved, shot released). The 500 ms rule applies only to script CPU between steps - never to a legitimate in-flight camera call. The step budget grows with your script automatically: the editor sizes it from the step estimate every time you save, so long multi-day loops just work.

Per-call deadlines bound long individual calls (a generous maximum per call type, enforced with a readable result instead of a freeze):

CallDeadlineResult on deadline
camera.focus()fixed 10 s status: timeout
camera.takePicture() / takePictureAF() / burst(n) camera confirmation windowstatus: timeout (the shot may still have fired)
camera.bulbShot(seconds)declared duration + 30 s status: timeout
location.refresh()10 s quick refresh; 5 min for the first fix after a cold start throws LocationUnavailable

A camera call blocked in the camera-loss wait state (below) is exempt from the watchdog and deadlines - the session policy, not the CPU limiter, governs that wait.

Camera loss & the trial cap

If the Wi-Fi link to the camera is lost mid-run, the session follows your camera-loss policy (the same values as the intervalometer: pause, continue, abort) and auto-reconnects:

The trial cap (50 shots in trial mode) is enforced by the app, not the script: once the cap is spent, camera.takePicture(), burst() and bulbShot() return trialLimit records, and the session force-stops the script (its next step receives ScriptStopped). No script logic can continue past the cap. session.trialCapRemaining lets well-behaved scripts plan ahead.

Writing conventions

Namespace summary

NamespaceDescription
cameraExposure control (shutter, aperture, ISO), focus control, shots, bursts, and bulb exposures.
sessionRead-only session state: status, photo count, shots remaining, running / time remaining, trial cap remaining.
exposureLight-amount math: scene EV for a setting and relative light ratios between settings.
skyOffline sun & moon tracker: named hours, live geometry, moon state, and custom altitude/azimuth window queries.
locationDevice GPS (no network): the cached session position and explicit refresh.
timeDevice wall time, suspension (sleep), and time-until math.
logTimestamped script console output, persisted with the run record.
rngDeterministic pseudo-random numbers (seeded per run).
constantsFrozen objects of named string constants for every status / mode / phase enum.
Shared shapes. Span - frozen { ms, s, min, h }: ms is the exact integer; s, min, h are the same duration in seconds/minutes/hours (fractions). Returned by session.runningTime, session.timeRemaining and time.until() - pick the unit you want. ExposureSettings - flat { shutter, aperture, iso } numbers (extra keys ignored, so camera.getExposure() is accepted wherever an ExposureSettings is expected). LocationFix - frozen { lat, lon, accuracy, timestamp }: decimal degrees, accuracy in meters, fix time as ISO datetime.

Namespace camera

Camera control over the camera's built-in web server on your local Wi-Fi. All calls are async and are steps. All exposure values are flat numbers (see conventions). Camera-level problems come back as statuses in the result record, except the three read calls, which throw CameraDisconnected when the link is still lost (see Camera loss).

FunctionDescription
getCapabilities()Snapshot of shooting modes, value ladders, and features.
getExposure()Current exposure settings, shooting mode, and settable flags.
setShutter(value)Set the shutter speed.
setAperture(value)Set the aperture (f-stop).
setIso(value)Set the ISO (or 'auto').
setExposure(settings)Set several exposure parameters in one call.
getFocus()Current focus state (AF/MF and AF method).
focus()Autofocus pass - no shot (half-press).
takePicture()Trigger one shot with the current focus.
takePictureAF()Autofocus + shot in one call.
burst(n)n shots at the camera's own continuous rate.
bulbShot(seconds)One-shot bulb exposure of a fixed duration.
bulbStart() / bulbStop()Manual bulb open/close.

camera.getCapabilities()

Description: One-shot snapshot of what the connected camera supports: available shooting modes, the exact value ladders per mode, and feature flags. Any value you set later is snapped to the nearest ladder step.

Returns: frozen { modes: string[], perMode: Record<string, { shutter: number[], aperture: number[], iso: Array<number | 'auto'> }>, features: string[] } - shutters in seconds (sub-second as fractions, e.g. 0.008 = 1/125); features is a subset of ['bulb', 'af', 'burst'] - check it before relying on a feature.

Errors: throws CameraDisconnected when the link is still lost (see Camera loss); otherwise blocks while the session waits for the camera.

const caps = await camera.getCapabilities();
// caps.perMode.M.shutter → [30, 15, 8, 4, 2, 1, 0.5, …, 0.008]
// caps.features → ['bulb', 'af', 'burst']

camera.getExposure()

Description: Reads the camera's current exposure settings, shooting mode, and which parameters are settable in that mode. settable is live - it changes automatically if the user turns the mode dial on the body.

Returns: frozen { shutter, aperture, iso, mode, settable } - flat numbers (iso may be 'auto'; a field the camera does not report is absent), mode a constants.MODE.* value, settable = { shutter: boolean, aperture: boolean, iso: boolean }.

Errors: throws CameraDisconnected when the link is still lost.

const ex = await camera.getExposure();   // { shutter: 1/4, aperture: 8, iso: 100, mode: 'manual', … }
const ev = await exposure.lightAmount(ex); // extra keys ignored - getExposure() works as ExposureSettings

camera.setShutter(value) · camera.setAperture(value) · camera.setIso(value)

Description: Sets one exposure parameter on the camera, honouring the current shooting mode. The value is snapped to the nearest supported ladder step. None of these accepts "bulb" - use camera.bulbShot(seconds).

Parameters: setShutter: seconds > 0 (sub-second as an ordinary numeric expression, 1/250; ≥ 1 s as 2, 30). setAperture: f-value > 0 (2.8, 8, 22). setIso: number > 0, or 'auto' where the camera supports it.

Returns: frozen { status, applied } - status = constants.EXPOSURE.ok | notSettable | busy | error (plus cameraDisconnected when the link is still lost). notSettable means the parameter is not settable in the current shooting mode - a status, not an error. applied = { shutter } / { aperture } / { iso } with the snapped value actually applied, or null when nothing was applied.

Errors: throws TypeError for an invalid value (non-numeric, ≤ 0, non-finite; setIso also rejects anything other than a number or 'auto'). Camera-level outcomes are in the record, never thrown.

const r = await camera.setAperture(8);
if (r.status === constants.EXPOSURE.notSettable) log.warn('aperture locked by mode');

camera.setExposure(settings)

Description: Sets several exposure parameters in one call, applied per parameter - a notSettable parameter does not block the others.

Parameters: settings - a flat object; any of shutter, aperture, iso may be present; only the fields given are touched. Field values follow the same rules as the individual setters.

Returns: frozen { status, applied } - applied maps each field that applied to its snapped value (empty or null when nothing applied); status is the least-good outcome across the given fields (ok | notSettable | busy | error, plus cameraDisconnected when the link is still lost).

Errors: throws TypeError if settings is not a flat object or a field value is invalid.

await camera.setExposure({ aperture: 8, iso: 100, shutter: 1 / 4 });

camera.getFocus()

Description: Reads the camera's current focus state.

Returns: frozen { mode, method } - mode = constants.FOCUS_MODE.af | mf (the camera's AF/MF switch), method = the camera's AF method as a raw string (camera-specific, e.g. single-shot / continuous AF).

Errors: throws CameraDisconnected when the link is still lost.

if ((await camera.getFocus()).mode === constants.FOCUS_MODE.af) { /* AF available */ }

camera.focus()

Description: Runs a standalone autofocus pass - no shot is taken (the camera's half-press / AF-only path). The focus stays where it landed: subsequent takePicture() calls reuse it without refocusing. This is how a script "locks" focus before a long run (time-lapse / astro pattern).

Returns: frozen { status } - status = constants.FOCUS.ok | notSettable | busy | timeout | cameraDisconnected.

Statuses: notSettable - camera in MF mode or no AF support; timeout - AF did not converge within the fixed 10 s per-call deadline. No shot is taken on any non-ok status.

const f = await camera.focus();            // focus once …
if (f.status === constants.FOCUS.ok) {
  while ((await time.until(goldenEnd)).ms > 0) {
    await camera.takePicture();            // … then shoot with focus locked
    await time.sleepS(30);
  }
}

camera.takePicture()

Description: Triggers one shot with the camera's current focus - it never re-triggers autofocus (focus is a separate action: focus() / takePictureAF()) and suspends until the camera responds. The shot counts (session counter, trial cap) only on the camera's confirmation.

Returns: frozen { status, shotNumber, takenAt } - status = constants.SHOT.ok | busy | timeout | cameraDisconnected | trialLimit; on ok, shotNumber = the session shot count and takenAt = ISO datetime; otherwise both are null.

Statuses: busy - a bulb exposure is open or another shot in flight; timeout - the camera did not confirm in time (the shot may still have fired); cameraDisconnected - link lost and not recovered (see Camera loss); trialLimit - cap spent: the shot is not taken and the session force-stops the script.

const r = await camera.takePicture();
if (r.status === constants.SHOT.ok) log.info('shot ' + r.shotNumber);

camera.takePictureAF()

Description: Autofocus and release in a single call - the camera's native AF+release path (one step, not two). Use for single shots where the subject may have moved; for multi-shot runs prefer focus() once + a takePicture() loop (no refocus per frame).

Returns: frozen { status, af, shotNumber, takenAt } - af = constants.FOCUS.* (the AF part), status = constants.SHOT.* (the shot part). When af is not ok, the shot does not happen and shotNumber / takenAt are null. af is null when no AF attempt ran at all (trial cap spent, a bulb open, or the link lost).

const r = await camera.takePictureAF(); // single portrait shot - subject may have moved

camera.burst(n)

Description: Takes n consecutive shots at the camera's own continuous rate in one call.

Parameters: n - integer ≥ 1 (anything else is a TypeError).

Returns: frozen array of exactly n takePicture-shaped records [{ status, shotNumber, takenAt }, …]; the same shot statuses apply per shot. Trial cap: if the cap is reached mid-burst, only as many shots as remain are fired and the tail of the array comes back as trialLimit records (the length stays n). If a bulb is open or the link is lost, every record is busy / cameraDisconnected respectively.

const results = await camera.burst(3); // one record per frame

camera.bulbShot(seconds)

Description: One-shot bulb exposure - the recommended bulb pattern: the engine starts the shutter, holds it for the duration, and stops it - one long call; the script suspends for the whole exposure. Can exceed the camera's maximum shutter time (e.g. 45 s on a 30 s-max body).

Parameters: seconds - number > 0, the exposure duration in seconds (10 = 10 s).

Returns: frozen { status, shotNumber, takenAt, exposure } - status = constants.SHOT.ok | busy | timeout | cameraDisconnected | trialLimit plus notSettable (camera without bulb support); exposure = the actual duration in seconds (measured; may slightly exceed the request) or null on failure; shotNumber / takenAt as in takePicture().

Statuses: busy - a bulb is already open; timeout - the closing command did not land within the per-call deadline (declared duration + 30 s); trialLimit as above.

await camera.bulbShot(45); // a 45 s astro exposure (see examples below)

camera.bulbStart() / camera.bulbStop()

Description: Manual bulb control for scripts that must intervene during the exposure. bulbStart() opens the shutter; bulbStop() closes it.

Returns: bulbStart() → frozen { status }; bulbStop() → frozen { status, exposure } with exposure = the actual open duration in seconds (camera-reported, or null when unavailable). Statuses: constants.EXPOSURE.* plus cameraDisconnected.

Statuses: busy - bulbStart() when a bulb is already open, or bulbStop() when no bulb is open; notSettable on cameras without bulb support. Calling takePicture() / bulbShot() while a bulb is open returns busy.

await camera.bulbStart();
while ((await sky.geometry()).sunAlt < -18) await time.sleepS(5);
await camera.bulbStop(); // exposure = the actual open duration in seconds

Namespace session

Read-only session-state variables. Plain values - reading them is not a host call, costs no step, and each is always current when read.

VariableTypeMeaning
session.statusconstants.SESSION.* scheduled | idle | running | paused | finished | error; paused = the camera-loss pause state; scheduled = waiting for a scheduled/astronomical start.
session.photoCountnumberShots confirmed this run.
session.shotsRemainingnumber | null Remaining until this run's effective shot cap (min of stop-condition N, time-limit-derived count, trial cap); null only when no cap exists (purchased + unbounded run).
session.runningTimeSpanElapsed this run.
session.timeRemainingSpan | nullUntil this run's stop condition.
session.trialCapRemainingnumber | null Shots left on the 50-shot trial cap; null when purchased.
while (session.status === constants.SESSION.running) { /* … */ }

Namespace exposure

Light-amount math over flat ExposureSettings. Pure computation.

exposure.lightAmount(settings)

Description: Computes the scene light / EV for an exposure setting. Formula: EV = log2(N²/t) − log2(ISO/100) - the ISO-100 exposure value corrected to scene light. Comparing EV across settings is only meaningful at constant ISO; for relative comparisons (different ISO) use lightRatio.

Parameters: settings - ExposureSettings (flat numbers; extra keys ignored, so camera.getExposure() is accepted directly). Pass a complete, valid setting.

Returns: number - scene EV, 1/3-EV precision. Lower = less light.

Errors: throws TypeError if the argument is not a settings object (missing or invalid components then simply yield NaN, not a throw).

const ev = await exposure.lightAmount(await camera.getExposure());
if (ev < 10) await camera.setExposure({ shutter: 1 }); // admit more light as it falls

exposure.lightRatio(a, b)

Description: Relative light comparison between two settings - covers cases (different ISO) that lightAmount cannot compare directly.

Parameters: a, b - complete, valid ExposureSettings (all three fields, positive numbers).

Returns: number - the 2n light factor between the two settings (2 = one stop more light in b than in a).

Errors: throws TypeError for an incomplete or invalid settings object.

await exposure.lightRatio({ shutter: 1/4, aperture: 8, iso: 100 },
                          { shutter: 1,   aperture: 8, iso: 100 }) // → 2 (one stop more light)

Namespace sky

Offline sun & moon tracker - computed on-device from location + time, no network. Every call uses the session position by default (the GPS fix acquired at session start, location.get()) and accepts an optional coords ({ lat, lon }) override - how a script plans a shoot at another known location. Dates default to today; date accepts 'today', 'tomorrow', or an explicit ISO date. All computations are pure and re-run on every call (the "today" / "now" defaults resolve at call time, so a run that spans midnight or days sees the values advance). All datetimes are ISO strings. Unknown option keys in the options object are a TypeError.

MemberDescription
sky.HOURSFrozen enum object - the named hours/points vocabulary (see table below).
hour(h, opts?)The daily occurrence of a named hour on a date.
next(h, opts?)Next occurrence of a non-daily event (full/new moon).
all(opts?)All named hours for a date in one object.
geometry(opts?)Live sun/moon altitudes, azimuths, shadow length & direction.
moon(opts?)Moon phase, age, illumination, usable-moonlight flag.
when(cond, opts?)Time ranges on a day inside an altitude/azimuth band.

sky.HOURS members: sunrise, sunset, solarNoon, goldenStart, goldenEnd, blueStart, blueEnd, civilDawn, civilDusk, nauticalDawn, nauticalDusk, astronomicalDawn, astronomicalDusk, astroDarknessStart, astroDarknessEnd, moonrise, moonset, moonTransit, fullMoon, newMoon.

Conventions: golden hour = sun 0° → −4°; blue hour = sun −4° → −6°; astro darkness = sun below −18° and moon below the horizon (the usable Milky-Way window). Full/new moon are non-daily events - use sky.next().

sky.hour(h, { coords?, date? })

Description: The occurrence of a daily named hour/point (sunrise, golden/blue hours, twilights, astro darkness, moon events) on a given date.

Parameters: h - a sky.HOURS member (strictly validated - an unknown value is a TypeError listing the allowed values). coords? - { lat, lon }, default the session position. date? - 'today' | 'tomorrow' or an explicit ISO date, default today. For midnight-crossing events (astro darkness), date is the day on which the night starts.

Returns: ISO datetime string, or null when the event does not occur on that date (polar day/night).

Errors: LocationUnavailable (no GPS fix - never a silent fallback position); TypeError for an invalid h.

const end = await sky.hour(sky.HOURS.goldenEnd);

sky.next(h, { coords?, after? })

Description: The next occurrence of a non-daily event after a given time - the only correct form for those events (they have no per-day meaning).

Parameters: h - sky.HOURS.fullMoon | sky.HOURS.newMoon. coords? as in sky.hour. after? - ISO datetime, default now.

Returns: ISO datetime string.

Errors: LocationUnavailable; TypeError for an unknown h.

const full = await sky.next(sky.HOURS.fullMoon);

sky.all({ coords?, date? })

Description: All named hours/points for a date in one object - the bulk form of sky.hour.

Returns: frozen object with one key per sky.HOURS member; each value is an ISO datetime or null (event not on that date).

Errors: LocationUnavailable.

const t = await sky.all(); // t.sunrise, t.astroDarknessStart, …

sky.geometry({ coords?, time? })

Description: Live solar/lunar geometry at a given time and location - not just named hours.

Parameters: coords? as in sky.hour; time? - ISO datetime, default now.

Returns: frozen { sunAlt, sunAz, moonAlt, moonAz, shadowRatio, shadowDir } - altitudes, azimuths and shadow direction in degrees; shadowRatio = 1 : x (shadow length per unit of object height).

Errors: LocationUnavailable.

if ((await sky.geometry()).sunAlt < -18) { /* sun below -18° */ }

sky.moon({ coords?, time? })

Description: Moon state at a given time and location.

Parameters: coords? as in sky.hour; time? - ISO datetime, default now.

Returns: frozen { phase, moonAgeDays, illuminationPct, usableMoonlight } - phase = constants.MOON.*, illuminationPct = number 0-100, usableMoonlight = boolean (moon above the horizon and illumination ≥ 50%).

Errors: LocationUnavailable.

(await sky.moon()).usableMoonlight

sky.when(cond, { coords?, date? })

Description: Custom-interval query - the time ranges on a given day where the sun is inside an altitude/azimuth band (e.g. "when is the sun behind the mountains (az 90-135 deg)?"). The workhorse for location planning. Single-day; for events that straddle midnight, date is the day on which the night starts and the range spans into the next day.

Parameters: cond - { altitudeBetween?: [lo, hi], azimuthBetween?: [lo, hi] } in degrees (at least one field required). coords?, date? as in sky.hour.

Returns: array of { start, end } ISO datetime ranges for that day (possibly empty, possibly several windows).

Errors: LocationUnavailable; TypeError for a missing/invalid cond.

const late = await sky.when({ altitudeBetween: [0, 5] }, { date: 'tomorrow' }); // late-golden window
const east = await sky.when({ azimuthBetween: [90, 135] });                      // sun over the eastern ridge

Namespace location

Device GPS - no network. A fix is acquired at session start and cached as the session position for the whole run (static, no periodic refresh). A missing fix does not block the session - it degrades sky features: location.get() returns null and sky.* calls without an explicit coords throw LocationUnavailable (never a silent fallback position).

location.get()

Description: Returns the cached session position - the fix every sky.* call is computed with. Pure cache read: no acquisition, the only API call that costs no step.

Returns: frozen LocationFix { lat, lon, accuracy, timestamp } (decimal degrees, meters, ISO datetime), or null when no fix exists.

Errors: none (null, not a throw).

const fix = await location.get();
if (fix && fix.accuracy > 200) log.warn('coarse fix');

location.refresh()

Description: User-initiated re-acquisition for when a new fix is really needed (setup moved, coarse initial fix): acquires a fresh fix, updates the session position (all subsequent sky.* calls use it), and returns it.

Returns: frozen LocationFix (the fresh fix - now the session position).

Errors: throws LocationUnavailable (permission denied / no fix within the deadline). Acquisition deadline: 10 s quick refresh when a fix already exists, 5 min for the first fix after a cold start / permission grant.

const fix = await location.refresh(); // e.g. after moving the tripod
log.info('now at ' + fix.lat + ', ' + fix.lon + ' ±' + fix.accuracy + ' m');

Namespace time

The only way to read time and the only way to wait - there are no host timers.

time.now()

Description: Device wall time.

Returns: number - ms epoch (ISO-convertible).

log.info('done at ' + (await time.now()));

time.sleep(ms) / time.sleepS(s) / time.sleepMin(min) / time.sleepH(h)

Description: Suspends the script for a duration (step suspension - the UI stays responsive). The only built-in way to wait.

Parameters: the duration in milliseconds / seconds / minutes / hours; must be a finite number ≥ 0 (fractions are OK for sleepS / sleepH, e.g. time.sleepS(0.5)). time.sleepS(1) ≡ time.sleep(1000).

Returns: none - the script resumes when the duration has elapsed.

Errors: throws ScriptStopped when a user stop is observed at the step; TypeError for a negative or non-finite duration.

await time.sleepMin(1);

time.until(when)

Description: Computes the time remaining until an ISO datetime. Read-only - it does NOT suspend (only time.sleep* suspends).

Parameters: when - ISO datetime string.

Returns: Span { ms, s, min, h } (≤ 0 once when has passed) - as with every API call this returns a Promise: await time.until(when).

Errors: none thrown - an unparseable when resolves to a zero span (loop guards built on .ms > 0 simply end the loop instead of crashing an unattended run).

while ((await time.until(goldenEnd)).ms > 0) { /* loop until golden end */ }

Namespace log

log.info(msg) / log.warn(msg) / log.error(msg)

Description: Writes a timestamped line to the script console (editor output panel); lines are persisted per run with the run record.

Parameters: msg - string (any value is coerced to a string).

Returns: none.

log.info('shot ' + r.shotNumber);

Namespace rng

rng() / rng(seed)

Description: Deterministic pseudo-random number generator - the only randomness available (Math.random is removed). The generator is seeded per run and the seed is recorded in the run record, so the same seed reproduces the same shot sequence.

Parameters: none - draw the next value; or seed - a finite number that re-seeds the run's generator (all later rng() draws continue from the new seed).

Returns: number in [0, 1).

Errors: TypeError for a non-finite seed.

const jitter = (await rng()) * 500; // 0-500 ms random jitter
await time.sleep(jitter);

Namespace constants

Frozen objects of string constants (string value = member name), one per closed vocabulary. Scripts compare against these named values instead of raw strings. sky.HOURS stays on sky with the same convention.

ObjectMembersCompared against
constants.SHOTok, busy, timeout, cameraDisconnected, trialLimit *.status from takePicture / burst / bulb calls
constants.EXPOSUREok, busy, notSettable, error *.status from the set* exposure calls and the bulb open/close calls
constants.FOCUSok, notSettable, busy, timeout, cameraDisconnected *.status from camera.focus() and the af field of takePictureAF()
constants.FOCUS_MODEaf, mf camera.getFocus().mode
constants.MODEmanual, aperturePriority, shutterPriority, program, auto camera.getExposure().mode
constants.MOONnew, waxingCrescent, firstQuarter, waxingGibbous, full, waningGibbous, lastQuarter, waningCrescent sky.moon().phase
constants.SESSIONscheduled, idle, running, paused, finished, error session.status

Camera calls can also report cameraDisconnected outside these lists (see Camera loss) - compare raw strings for that value or simply handle "not ok".

if (r.status === constants.SHOT.ok) { /* shot confirmed */ }

Errors & statuses

Errors are thrown, catchable JS errors. Statuses are values returned inside the result record - inspect them, do not expect a throw.

Error / statusMeaningNotes
ScriptStoppedUser stop requested (or the session force-stopped the run, e.g. on the trial cap); clean exit path. Catchable - a well-behaved script unwinds gracefully.
ScriptLimitExceededStep/CPU/memory budget exhausted. Catchable (once); further budget consumption kills the run. Counters are preserved.
LocationUnavailableNo GPS fix (permission denied / cold start) or acquisition timed out. Catchable. Never a silent fallback position - pass coords to sky.* to plan without a fix.
CameraDisconnectedWi-Fi link to the camera lost and not recovered. Catchable. Thrown by the camera read calls (getCapabilities, getExposure, getFocus); shot and set calls instead return a cameraDisconnected status. The session follows your camera-loss policy (pause / continue / abort) and auto-reconnects.
TypeErrorInvalid argument (non-numeric value, malformed settings, unknown enum member, unknown option, negative sleep, non-finite seed). The message states what was expected. This is an input mistake, not a camera condition.
SHOT.busy / timeoutCamera did not accept the shot in time. Statuses - scripts implement retry logic.
EXPOSURE.notSettableParameter not settable in the camera's current shooting mode. Status, not an error - check (await camera.getExposure()).settable first, or handle the status and adapt.
FOCUS.notSettable / timeoutMF mode or no AF support / AF did not converge within 10 s. Statuses - no shot is taken; scripts can retry or continue with the last focus.
SHOT.trialLimitTrial cap (50 shots) reached. Status - do not rely on the script: the session engine force-stops the script on the first trialLimit.
*.cameraDisconnectedWi-Fi link to the camera lost and not recovered. Status on shot/set calls - the session follows your camera-loss policy; on a paused policy an in-flight call simply waits for the reconnect instead.

Example scripts

Golden-hour time-lapse, stop when light drops

// Every host-API call is async - await them all, including read-only queries.
log.info('Golden hour time-lapse');
const goldenEnd = await sky.hour(sky.HOURS.goldenEnd);
if (goldenEnd !== null) {
  await camera.setExposure({ aperture: 8, iso: 100, shutter: 1 / 4 });
  // the trial cap is enforced by the app - no script-side bookkeeping
  while ((await time.until(goldenEnd)).ms > 0) {
    const r = await camera.takePicture();
    if (r.status === constants.SHOT.ok) log.info('shot ' + r.shotNumber);
    await time.sleepS(30); // every 30 s
    // slow down (lengthen) the shutter as light falls - lower scene EV means
    // less light, so admit more: 1/4 s → 1 s is +2 stops (at fixed ISO)
    const ev = await exposure.lightAmount(await camera.getExposure());
    if (ev < 10) await camera.setExposure({ shutter: 1 });
  }
} else {
  log.warn('No golden-hour end today - nothing to do');
}
log.info('done at ' + (await time.now()));

Astro window: 45 s bulb exposures only while truly dark and the moon is out of the way

// Overnight / multi-day loop: runs until stopped. For runs of several days
// the step budget grows to match automatically when you save (see the step estimate).
while (session.status === constants.SESSION.running) {
  const g = await sky.geometry();
  // "astro dark": sun below -18° AND moon below the horizon
  if (g.sunAlt < -18 && g.moonAlt < 0) {
    await camera.setExposure({ aperture: 2.8, iso: 3200 });
    await camera.bulbShot(45); // one-shot 45 s bulb - the call IS the long exposure
  }
  await time.sleepMin(1);
}

Two-day sky time-lapse: star frames at night, day exposure after sunrise

// Lock focus once, then shoot every 30 s for 48 h. Night frames (10 s, wide,
// high ISO) and day frames (1/500 s, low ISO) switch automatically on the sun's
// altitude; a lost location fix keeps the last known mode instead of stopping.
const end = (await time.now()) + 2 * 24 * 3600 * 1000;
await camera.focus();
let night = true;
while ((await time.now()) < end) {
  try {
    night = (await sky.geometry()).sunAlt < -6;
  } catch {} // no fix right now - keep the last known mode
  await camera.setExposure(night
    ? { shutter: 10, aperture: 2.8, iso: 3200 }
    : { shutter: 1 / 500, aperture: 8, iso: 100 });
  const r = await camera.takePicture();
  if (r.status !== constants.SHOT.ok) log.warn('shot failed: ' + r.status);
  await time.sleepS(30);
}
log.info('sky time-lapse done: ' + session.photoCount + ' frames');