npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pulze-io/renderflow

v2.0.20

Published

Official RenderFlow API client for TypeScript/JavaScript

Readme

@pulze-io/renderflow

Official TypeScript/JavaScript client for the RenderFlow API.

Installation

npm install @pulze-io/renderflow

Quick 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)