@pulze-io/renderflow
v2.0.20
Published
Official RenderFlow API client for TypeScript/JavaScript
Maintainers
Readme
@pulze-io/renderflow
Official TypeScript/JavaScript client for the RenderFlow API.
Installation
npm install @pulze-io/renderflowQuick start
import { RenderFlow } from "@pulze-io/renderflow";
const rf = new RenderFlow({
apiKey: process.env.RENDERFLOW_API_KEY
});
const { data, total } = await rf.jobs.getJobs({ status: "working" });
console.log(`${data.length} of ${total} jobs`);baseUrl defaults to http://localhost:44442/api/v1, which is the service on this machine and
needs no credential — how a plugin on a workstation submits. Point it at a server
(https://rf.studio.lan:44445/api/v1) and pass an apiKey created in the UI under Settings.
timeout is per request and defaults to 30s. A call resolves to the response body and rejects on
any non-2xx.
Finding the operation you want
Methods are named after the API document's operationId, so a route reads the same here, in the
Python SDK, in rfcli and in the MCP tools. There is no shorter alias layer — the previous one
drifted from the API and was removed.
| you want | you write |
| --- | --- |
| GET /jobs | rf.jobs.getJobs() |
| GET /jobs/{id} | rf.jobs.getJob(id) |
| POST /jobs | rf.jobs.createJob(body) |
| POST /jobs/{id}/stop | rf.jobs.stopJob(id) |
| GET /nodes | rf.nodes.getNodes() |
The groups are jobs, jobsArchived, tasks, nodes, pools, users, userGroups,
schedule, renderLicenses, errors, webhooks, info and service — 98 operations in all.
Every request and response type is exported alongside them.
Submitting a job
A recipe is the short way in: type, the file, and the flat per-DCC fields. The service expands it
into the step tree. file, host and type are required on every recipe.
const job = await rf.jobs.createJob({
type: "3dsmax.render",
name: "Exterior Shot",
file: "//share/projects/scene.max",
host: { id: "3dsmax", version: "2026" },
engine: { id: "vray", name: "V-Ray", version: "7.20.04" },
frame: "1-100",
resolution: "1920x1080",
priority: 75
});
await rf.jobs.startJob(job.id);frame is a single frame, a range, a range with a step (1-100:4) or a list. resolution is a
WIDTHxHEIGHT string. A submitted job starts pending unless you pass status: "suspended".
createJob answers 202 with a job still being built — pass { wait: "true" } as the second
argument to block until it is ready and get a 201 instead.
For a multi-step job, send a JobCreate with its own steps array instead of a recipe; both
shapes are the one JobSubmit union.
Live updates
One socket, shared by every subscription, opened on the first and closed with the last. No polling.
const stop = rf.jobs.on(event => {
if (event.op === "resync") return refetch(); // events were missed — read again
console.log(event.op, event.id, event.patch);
}, { id: job.id });
stop();on is on jobs, tasks and nodes. The second argument narrows it: id on any of them,
step_id on tasks. An array is one subscription over several ids, not several subscriptions.
resync arrives in the same stream as the changes, and it is not one. The socket has no replay,
so a reconnect, a dropped frame, or a client the farm outran all mean the same thing: this collection
has to be read again. It comes through the handler rather than a second callback precisely so there
is nothing to forget to pass.
rf.live.close() drops everything; rf.live.onError reports a refused credential, and writes to
stderr until you replace it. Node gained a global WebSocket in 22 — below that, pass one as the
WebSocket option.
Waiting for a job
const job = await rf.jobs.settled(jobId); // resolves on completed or failed
await rf.jobs.settled(jobId, { timeout: 3600_000 });null means the job is no longer there. Archiving and deleting are the same event on the socket, so
either way it is not going to finish.
A stop is not an end: suspending a job is resumable, so pass
{ statuses: ["completed", "failed", "suspended"] } if a stop is an answer for your caller. There is
no default timeout, because a long render legitimately takes days.
This exists because the obvious version is wrong in four ways: it stops on completed only, never
sees a delete, hangs forever on a job that had already finished before it subscribed, and reads
before it subscribes — which loses a job that finishes in between.
Managing jobs
const job = await rf.jobs.getJob(id);
await rf.jobs.updateJob(id, { priority: 100, limit: 5 });
await rf.jobs.updateJobPool(id, { pool_id: poolId });
await rf.jobs.stopJob(id);
await rf.jobs.resetJob(id);
await rf.jobs.deleteJob(id);
// Many at once, by id
await rf.jobs.updateJobs({ ids: [a, b], values: { priority: 50 } });
await rf.jobs.stopJobs({ ids: [a, b] });
// Counts for the dashboard
const summary = await rf.jobs.getJobSummary();Archiving is its own group, because an archived job is a different collection:
await rf.jobsArchived.archiveJobs({ ids: [id] });
const { data } = await rf.jobsArchived.getArchivedJobs({ limit: 50 });
await rf.jobsArchived.restoreArchivedJobs({ ids: [id] });Steps
const step = await rf.jobs.getJobStep(jobId, stepId);
await rf.jobs.startJobStep(jobId, stepId);
await rf.jobs.stopJobStep(jobId, stepId);
await rf.jobs.resetJobStep(jobId, stepId);
// The whole step's output, from one machine — `node_id` is required, because a step's log is
// written per node. A negative offset counts back from the end.
const logs = await rf.jobs.getStepLogs(jobId, stepId, { node_id: nodeId, offset: -200 });Tasks
// Tasks are filtered by query, not by path
const { data, total } = await rf.tasks.getTasks({ job_id: jobId, status: "working", limit: 50 });
const task = await rf.tasks.getTask(id);
// `lines`, plus `total`/`offset` for paging and `source` for where they came from
const { lines } = await rf.tasks.getTaskLogs(id, { offset: -200 });
// A rendered frame. Resolves to a Blob. `size` is `thumb`, `preview` or `full` — `full` is
// transcoded on demand.
const preview = await rf.tasks.getTaskPreview(id, { size: "thumb" });
await rf.tasks.resetTasks({ ids: [id] });Nodes
const { data } = await rf.nodes.getNodes({ status: "busy" });
// `extend` joins hardware, software and benchmark data
const node = await rf.nodes.getNode(id, { extend: "true" });
await rf.nodes.updateNodeStatus(id, { status: "suspended" });
await rf.nodes.updateNodePool(id, { pool_id: poolId });
const ranking = await rf.nodes.getNodeBenchmarkRanking({ type: "vray" });
const hardware = await rf.nodes.listNodeHardware({ limit: 50 });
const tags = await rf.nodes.getNodeTags();Pools, users and groups
const pools = await rf.pools.getPools();
const pool = await rf.pools.createPool({ name: "GPU" });
const { data: users } = await rf.users.getUsers();
await rf.users.setUserNode(userId, { node_id: nodeId });
const groups = await rf.userGroups.getUserGroups();Schedule and render licences
const events = await rf.schedule.getSchedule({ enabled: true });
const occurrences = await rf.schedule.getScheduleOccurrences(eventId, { count: 10 });
await rf.schedule.runScheduleEvent(eventId);
// An event's `action` is either a command against machines or a job to submit. Commands are the
// `ScheduleCommand` enum, exported alongside the client.
import { ScheduleCommand } from "@pulze-io/renderflow";
await rf.schedule.createScheduleEvent({
name: "Nightly",
every: "1d",
action: { kind: "command", type: ScheduleCommand.NodeWakeup },
targets: { all: true }
});
const licences = await rf.renderLicenses.getRenderLicenses();
const presets = await rf.renderLicenses.getRenderLicensePresets();Errors
const { data } = await rf.errors.getErrors({ limit: 50 });
// Worst level and count, grouped by node, by job, or by the pair
const summary = await rf.errors.getErrorSummary({ by: "job", ids: jobId });Service
const info = await rf.info.getInfo();
console.log(`RenderFlow ${info.version}`);
// The one operation that needs no credential
const alive = await rf.service.getServiceAlive();How this package is built
src/api.ts is generated from the service's openapi.public.json — the x-access: public
operations and the schemas they reach. Two files are hand-written, because the document describes
neither: src/index.ts (the constructor) and src/live.ts (the socket behind on).
npm run generate # openapi.public.json -> src/api.ts
npm run build # dist/ (cjs + esm + .d.ts)