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

render-workflows-dart

v0.8.4

Published

Unofficial: write Render Workflows tasks in Dart, compiled to JavaScript. Not affiliated with or endorsed by Render.

Readme

render-workflows-dart

Renamed. This package was render-dart up to 0.8.1 and is render-workflows-dart from 0.8.2. The old render-dart command still works as an alias, so an existing project only changes one line:

-    "render-dart": "^0.8.1"
+    "render-workflows-dart": "^0.8.2"

This is an unofficial, independent, community-built package. Not affiliated with, endorsed by, or supported by Render.

Render's own SDKs and documentation are at render.com/docs.

Render *

Write Render Workflows tasks in Dart, on Render.

Render Workflows is in public beta and has no built-in way to use Dart: tasks are defined with Render's own SDK, which is available for TypeScript and Python only. Their docs say SDKs for more languages are planned.

render-workflows-dart provides first class dart language support, without reaching for Docker (which would cost API provisioning and local development). It can compile Dart two ways:

  • Task bodies to JavaScript, with dart compile js, registered through Render's @renderinc/sdk. Render sees an ordinary Node workflow.
  • Anything needing the real platform to a native executable, with dart compile exe. The JavaScript side becomes a generated shim that calls it, so the task code still reads as a plain Dart call. That is what reaches dart:io, dart:ffi, isolates, and packages like postgres that have no web build at all.

Both are compiled during the deploy, on Render's own hardware — nothing binary is committed. You get real static analysis over the whole thing.

The Dart side of Render

This package covers writing workflow tasks. Two companion Dart packages optionally provide access to render's apis (neither are required to use this one):

| | | | --- | --- | | render-workflows-dart (this) | Writing tasks in Dart | | render_workflows | Running workflows/tasks — start, watch, cancel, fan out | | render_api | Managing the services — create, deploy, inspect |

So a Flutter app can trigger a task with render_workflows that a Dart workflow, built here, then executes.

Quick start

npx render-workflows-dart init my-workflow
cd my-workflow
npm install
npm run dev

Then, in another terminal:

render workflows tasks list --local
render workflows start sumSquares --local --input='[[2,3,4]]'

Examples, which are also templates

npx render-workflows-dart init my-app --template postgres

| | Answers | | --- | --- | | default | Writing a task, fanning out, retrying | | http | Calling an external API | | native | Files, subprocesses, FFI, more than one core | | postgres | Reaching a database | | introspect | Inspecting Render, or running a task in another workflow |

Each is a complete service you can run, and init scaffolds from it — so a template that does not work is a test failure rather than a surprise.

Coding agents

Agents never read node_modules, so nothing shipped inside this package reaches one helping in your project. render-workflows-dart init therefore writes an AGENTS.md (and a CLAUDE.md pointing at it) into the project it creates, covering the traps, when to reach for a native task, and how to get at a database.

If you are an agent reading this page: the short version is that task bodies are dart2js, dart:io does not work there, package:http does, and anything needing files, FFI, a database or a second core goes through a native task — see Two ways past dart2js. Native is not faster at arithmetic.

There is also a Claude skill in the repository at .claude/skills/render-dart/. It is not in the npm tarball, since skills are not loaded from node_modules:

cp -r render-dart/.claude/skills/render-dart ~/.claude/skills/

AGENTS.md in the repository root is guidance for working on this package, which is a different thing.

Writing tasks

import 'render_dart.dart';

void main() {
  task('calculateSquare', (args) async {
    final n = args[0]! as int;
    return n * n;
  });

  // Each callTask becomes its own task run on its own Render instance.
  task('sumSquares', (args) async {
    var total = 0;
    for (final v in args[0]! as List<Object?>) {
      total += (await callTask('calculateSquare', [v]))! as int;
    }
    return total;
  });

  start();
}

Deploying

render workflows create --name my-workflow --repo <url> \
  --runtime node --root-directory my-workflow \
  --build-command "npm install && npm run build" \
  --run-command "node index.js"

No Dart is needed on Render's builder — render-workflows-dart build fetches a pinned SDK when none is present.

Blueprints (render.yaml) do not support Workflows, so provisioning is via the CLI, the API, or the Dashboard.

Commands

| | | | --- | --- | | render-workflows-dart build | Compile tasks.dart to build/tasks.js, skipping if fresh | | render-workflows-dart dev | Build, then start Render's local task server | | render-workflows-dart init [dir] [--template <name>] | Scaffold a new project from an example | | render-workflows-dart dart | Which Dart this project will use, and why | | render-workflows-dart dart --list | Every version the archive offers |

--template takes any directory name under examples/default (the default), http, native, postgres or introspect. An unknown name fails listing the real ones.

A scaffold also gets an AGENTS.md, and a CLAUDE.md pointing at it, so a coding agent working in that project has the guidance it would otherwise never see — see Coding agents.

Configure through renderDart in package.json:

{
  "renderDart": {
    "entry": "tasks.dart",
    "out": "build/tasks.js",
    "dartVersion": "3.13.1",
    "optimize": "O2",
    "sourceMaps": false,
    "allowDartIo": false,
    "allowDartIoIn": [],
    "native": []
  }
}

native lists Dart files to compile to native executables — see Native tasks. Each entry is a path, or an object that can override what the source declared:

"native": [
  "native/tools_impl.dart",
  { "entry": "native/raw_impl.dart", "mode": "exe" },
  { "entry": "native/hot_impl.dart", "worker": false }
]

allowDartIoIn exempts named directories from the dart:io guard — for a local tool sitting beside the workflow rather than running on it, like the seeder in the postgres example. Narrower than allowDartIo: true, which switches the check off for task code too.

Choosing a Dart version

Three places, highest first. The flag is for trying one once, the environment for varying a build without a commit — Render's dashboard sets those — and package.json for the answer that should travel with the project:

npx render-workflows-dart build --dart-version 3.12.2
RENDER_DART_VERSION=3.12.2 npx render-workflows-dart build
"renderDart": { "dartVersion": "3.12.2" }

A version can be exact, latest, or a channel name — stable, beta or dev. An exact version needs no network to interpret, so a pinned project keeps building when the archive is unreachable; an alias is resolved against the archive on each build, which is the point of asking for one.

npx render-workflows-dart dart          # what this project will use, and why
npx render-workflows-dart dart --list   # 176 stable releases, newest first

A pin set explicitly wins over a Dart already on PATH. If they differ, the requested version is downloaded and used. Only the built-in default defers to a local toolchain — it exists so a first build on Render has something to fetch, not to override a Dart you installed deliberately.

Before 0.8.0 that was not true: the pin was consulted only when downloading, so it worked on a first Render build and was silently ignored everywhere else. On a laptop PATH always won, and on later Render builds whatever had been vendored first won for ever, because the cache key was "does the directory exist". If setting dartVersion ever appeared to do nothing, that is why. The vendored SDK now records its version and is replaced when the pin changes.

Every build says which Dart it used and where it came from:

[render-dart] Dart 3.12.2 requested by RENDER_DART_VERSION
[render-dart] using Dart 3.12.2 (downloaded)

Downloads are checked against the archive's published SHA-256 before being unpacked. The hash is computed while the archive streams to disk, so it costs no extra I/O — 0.18s of CPU for a 228 MB file, against roughly 30s to fetch it. Releases old enough to predate the published sums say so rather than implying a check happened.

Using pub.dev packages

Add them to pubspec.yaml as normal; render-workflows-dart build runs dart pub get for you. What works is determined by dart2js, not by Render:

| | Works? | | | --- | --- | --- | | Pure Dart (collection, crypto, intl, path) | yes | Nothing to think about | | package:http | yes | Goes through fetch, which Node 18+ provides | | Anything importing dart:io | no | File, Process, Socket, HttpClient |

dart:io is the trap. dart2js compiles it without complaint and then throws Unsupported operation at runtime — so a task using it deploys cleanly and fails on its first real run, potentially burning up to the task timeout first. render-workflows-dart build therefore refuses to build a project that imports dart:io directly, and tells you what to use instead. Conditional imports (if (dart.library.io)) are left alone, and allowDartIo opts out.

For Node APIs beyond HTTP, use dart:js_interop directly.

WebAssembly

Packages that ship a .wasm module work, and they are often the simpler choice: the module runs inside the Node process, so there is no subprocess, no second binary, and no build step beyond the one you already have.

When a package has no wasm build — or the work needs files, sockets or more than one core — native tasks are the other route.

Two shapes, both verified on Render, differing in how much the runtime has to supply:

| | Needs | Verified | | --- | --- | --- | | Uses the platform's WebAssembly API (forge2d) | asset resolution only | Box2D v3, zero config | | Carries a JS wasm runtime (rust_cryptowasm_run) | asset resolution, XMLHttpRequest, and two pre-seeded globals | SHA/MD5/HMAC, cross-checked against pure-Dart crypto |

wasm_run looks browser-only at first: it loads its WASI shim by injecting a <script> tag into an HTML document. But its setup checks whether the global is already present and skips injection if so. The runtime seeds both: wasmFeatureDetect comes from a UMD bundle shipped inside the pub package, and browser_wasi_shim from npm. It then loads its module over XMLHttpRequest, which the runtime also provides, on top of fetch.

For wasm_run-based packages, add the shim to your project — it is an optional peer dependency, so nothing else pays for it:

npm install @bjorn3/browser_wasi_shim

Then the package works unmodified, with no loadModule callback and no other setup. Its native path stays unavailable, needing dart:ffi and a wasmtime binary; the web executor is what runs here, on the host's own WebAssembly.

One caveat worth knowing before combining packages: rust_crypto and forge2d 0.15 cannot share a pubspec, because wasm_run pulls build_rust_binarieshooks ^1.0.0 while forge2d needs hooks ^2.0.0. Put them in separate workflows.

forge2d — a dart:ffi binding to Box2D v3 — selects a bundled 227 KB WebAssembly build under dart2js, and runs on Render unchanged:

await initializeForge2D(wasmUri: Uri.parse(fileUri('web/box2d.wasm')));

No configuration, no staging step, no wasmUri.

A Dart web app serves each package's lib/ at packages/<name>/, and packages that ship assets ask for them at exactly that path. Nothing serves it under Node, so the request fails. The runtime resolves those paths from .dart_tool/package_config.json — written by dart pub get, so the mapping is exact rather than guessed — and reads the file directly. Node's fetch also has no file: scheme, which the runtime adds for the same reason.

For assets of your own rather than a package's, fileUri() resolves a project-relative path:

final data = await http.get(Uri.parse(fileUri('data/table.json')));

node:wasi is not required: forge2d supplies its own WASI shims. It is available in Node if a module ever needs the real thing.

Reaching Node from a task

dart:io compiles under dart2js and then throws at runtime, so a task cannot open a file, spawn a process, or reach the npm ecosystem on its own. Two helpers close that gap.

Any npm package or Node built-in:

@JS()
extension type _Crypto(JSObject _) implements JSObject {
  external String randomUUID();
}

final crypto = _Crypto(requireModule('node:crypto'));
print(crypto.randomUUID());

Dart cannot call require itself — in CommonJS it is module-scoped, and globalThis.require is undefined in both CommonJS and ESM — so the runtime hoists it. Resolution is rooted at your project directory, so requireModule('lodash') means whatever your package.json depends on.

Shelling out to a CLI tool. (For calling Dart compiled natively, use native tasks rather than driving a process by hand.)

final result = await runProcess('git', args: ['rev-parse', 'HEAD']);
if (result.ok) print(result.stdout.trim());

runProcess takes args, workingDirectory, environment, stdin, timeout and runInShell. A non-zero exit is returned, not thrown — an exit code is a result, and the caller usually wants stderr with it. It throws only when the process could not be started, or when timeout elapses (SIGKILL, since a task run is already bounded by Render's own timeout).

Two ways past dart2js

dart2js cannot open a file, use a second core, or run a package that needs dart:io. There are two escapes, and they are complementary rather than ranked.

| | WebAssembly | Native task | | --- | --- | --- | | Runs in | the Node process | a subprocess | | Needs | the package to ship a .wasm | nothing — any Dart compiles | | dart:io, sockets, files | no | yes | | dart:ffi | no | yes | | More than one core | no | yes, isolates | | Per-call cost | none | ~0.5 ms with a worker | | Extra artefact | none | a binary, built during the deploy |

Reach for wasm when the package already has one. forge2d and rust_crypto both do, and render-workflows-dart resolves their modules without configuration. Nothing is spawned and nothing is compiled.

Reach for native when there is no wasm build, when the work needs I/O or FFI, or when it needs to use more than one core. package:postgres is the clearest case: it speaks the wire protocol over a raw socket, and pub.dev marks it runtime:native-aot with no runtime:web. There is no wasm alternative and no dart2js path — native or nothing.

What native is not

It is not a way to make computation faster. The same recursive fib, compiled both ways and run on Render:

| n | dart2js | native | | --- | --- | --- | | 30 | 8 ms | 23 ms | | 34 | 60 ms | 50 ms | | 36 | 146 ms | 131 ms |

V8 matches Dart AOT on pure integer work, and beats it at small n. If your task is arithmetic, dart2js is already fine.

The speed win that is real is parallelism, because dart2js inherits JavaScript's single thread. The same batch of fib(32), run sequentially in JavaScript and across isolates natively:

| jobs | dart2js seq | native parallel | speedup | | ---: | ---: | ---: | ---: | | 2 | 46 ms | 31 ms | 1.5x | | 4 | 88 ms | 48 ms | 1.8x | | 8 | 177 ms | 84 ms | 2.1x | | 16 | 367 ms | 219 ms | 1.7x | | 32 | 706 ms | 533 ms | 1.3x |

Treat that as anecdote. It is one workload on one Render instance, on the default starter task plan in a free workspace — the smallest there is. A different plan, or different work, would produce a different curve.

What it does illustrate is a shape worth expecting: the benefit is real, it does not grow indefinitely, and past some point more isolates cost more than they return. The dart2js column stays flat at ~22 ms per job throughout, which is the control confirming the native side's rise is not noise.

Platform.numberOfProcessors reported 32 the whole time, which was not a useful guide to any of this. Measure the workload on the plan it will run on.

Native tasks

Write the function once, compile it AOT, and call it from task code as if it were local — no process handling, no serialisation, nothing at the call site that says it is native.

Write the implementation in <name>_impl.dart:

// native/tools_impl.dart
import 'dart:io';

import '../native_task.dart';

@nativeTask
Map<String, Object?> inspect(String path) => {
      'bytes': File(path).lengthSync(),
      'lines': File(path).readAsLinesSync().length,
    };

Declare it, and call it by its plain name:

"renderDart": { "native": ["native/tools_impl.dart"] }
// tasks.dart — nothing here says "native"
import 'native/tools.dart';

task('inspect', (args) async => await inspect(args[0]! as String));

render-workflows-dart build generates native/tools.dart as a conditional export:

export 'tools.stub.dart' if (dart.library.io) 'tools_impl.dart';

so the same source compiles to a process call under dart2js and a direct call natively. That also means native code can be unit-tested on the Dart VM, and a native function calling a sibling skips the process hop entirely.

Always await a native task — the stub returns a Future where the implementation may return a plain value, and awaiting is what makes one piece of code valid on both sides.

What can cross

Parameters and return values are JSON, so: bool, int, double, num, String, List<T>, Map<String, T>, Object?, dynamic, and Future<T> of those, nullable included. Required, optional and named parameters all work, with their defaults.

Anything else — a custom class, Uint8List, Set, a record — is rejected at build time, naming the parameter, rather than failing as a decode error on a live run.

Options ride with the declaration

So a call site never has to know, and never has to be updated when you change your mind:

@NativeTask(worker: true, idleTimeout: Duration(seconds: 30))
Future<int> hot(int a) async => a;

| | | | --- | --- | | worker | Keep the executable alive between calls. Default false | | idleTimeout | How long an idle worker lingers. Default 30 s | | timeout | How long one call may take. Default none |

renderDart.native can override any of them per entry, so a deployment can change behaviour without editing code. To vary them for one caller — without changing any signature, which is what keeps the one-source property:

await NativeCall.scope(worker: false, () async => hot(1));

Worker mode

Measured on Render, 20 calls:

| | processes | time | | --- | --- | --- | | spawn per call | 20 | 112 ms | | worker | 1 | 9 ms |

It is opt-in because a worker keeps top-level state between calls. That is what makes it fast, and it also means a leak accumulates instead of being cleaned up by process exit, and one call can observe what the last one left behind. A call that throws does not kill the worker; a process that dies rejects everything in flight with its exit code and stderr, then respawns on the next call.

The wire, and errors

One JSON object per line (JSONL) over stdin/stdout. print() on the native side arrives as a $log line and is forwarded to the task log — on stdout it would corrupt the framing, so it is rerouted rather than left to break things. A native throw arrives as a NativeTaskException carrying the real message and the native stack trace.

A worked example: Postgres

package:postgres speaks the wire protocol over a raw socket. pub.dev marks it runtime:native-aot and runtime:native-jit, with no runtime:web — it cannot run under dart2js at all, and there is no wasm build to fall back on. Native is the only route to a database from a Dart workflow.

// native/db_impl.dart
@NativeTask(worker: true, idleTimeout: Duration(minutes: 2))
Future<List<Map<String, Object?>>> listWidgets({int limit = 20}) async {
  final db = await _db();                       // held open between calls
  final rows = await db.execute(
    Sql.named('select * from widgets limit @limit'),
    parameters: {'limit': limit},
  );
  return rows.map(_jsonRow).toList();
}
// tasks.dart
import 'native/db.dart';

task('listWidgets', (args) async => await listWidgets(limit: 20));

Worker mode earns its keep here: the process stays alive, so the TCP handshake, TLS negotiation and Postgres authentication happen once rather than per call. pg_backend_pid() proves it from the server's side — it stays constant across calls while a counter climbs.

Two things this example ran into, both worth knowing before you hit them:

  • timestamptz arrives as a DateTime, which is not JSON. Convert before returning, or the build rejects the signature — the right failure, but a puzzling one if unexpected.
  • A held connection can be dropped by the server, a deploy, or idling. Check and reconnect rather than surfacing a broken socket; that is the honest cost of keeping state in a worker.

A full version, with a local seeding program that creates the table over the external connection string while the tasks read it over the internal one, is in examples/postgres — which is also what init --template postgres scaffolds.

mode: "exe"

For a program that owns its own main() and wants no wrapper. It is compiled to build/native/<name> and left alone; call it with runProcess.

Building

The vendored SDK carries gen_snapshot, so this needs nothing extra — and nothing is cross-compiled and no binary is committed. Render's build host is already linux/x64, so the executable is produced from the source in the commit that deploys it.

Generated files (tools.dart, tools.stub.dart) are listed in a native/.gitignore the build maintains, because the facade takes a plain name and would otherwise read as hand-written source.

Native sources need dart:io, so declared native directories — and native_task.dart — are exempt from the dart:io guard. Everything else stays strict.

Two things this package exists to get right

RENDER_SDK_AUTO_START must be false before the SDK loads. The SDK's task() schedules its own startTaskServer() via setImmediate. Combined with an explicit start, that produces two task servers and runs every task body twice — doubled side effects and doubled billing. Neither render workflows dev nor Render sets this for you.

Dart must never throw across the JS boundary. A Dart exception converted by Future.toJS reaches Render as the opaque "Dart exception thrown from converted Future…", with the real message boxed out of reach. Task bodies return a {$ok}/{$err} envelope instead, and the runtime rethrows a genuine Error, so the actual message lands in the run record.

Both are handled for you. They are documented because they cost real debugging time to find.

Build caching on Render

The Dart SDK is unpacked into node_modules/.dart-sdk, and the pub cache into node_modules/.pub-cache. Render preserves node_modules between builds but not arbitrary top-level directories — measured, with the SDK elsewhere it was re-downloaded on every deploy, 33s of a 52s build. Cached, the build step is about a second.

Native executables are cached the same way, in node_modules/.native-cache, keyed on the content of their sources rather than mtime — every deploy is a fresh git checkout that restamps mtimes, so an mtime-keyed cache could never hit. A deploy that changes only tasks.dart reuses the executable instead of paying for another AOT compile.

Use Clear build cache & deploy in the Dashboard to force a clean fetch.

Layout

src/runtime.js        Loaded by your workflow; bridges Dart to the SDK
src/web-shims.js      Browser-shaped APIs Node lacks: self, file: fetch,
                      Dart package assets, XMLHttpRequest
src/node-bridge.js    Node access Dart lacks: require, subprocesses
src/native-worker.js  Keeping native executables alive between calls
src/cli.js            build / dev / init
src/toolchain/        SDK resolution and compilation, free of Render
                      specifics so it can be extracted later
dart/generator/       Reads @nativeTask with package:analyzer and writes
                      the dispatcher, stubs and facade. Its own pubspec,
                      so your project never depends on the analyzer
examples/             Five runnable services, which are also the `init`
                      templates — so a template cannot drift from a
                      working example
runtime/              Copied into a scaffold: the two Dart bridge files
                      (render_dart.dart, native_task.dart) and the
                      AGENTS.md that lands in your project

Licence

MIT

Note

* The Render name and logo are trademarks of Render Services, Inc. The mark itself is unmodified, shown on white with the clear space Render's brand kit specifies, referentially — to identify the service these packages work with, not to suggest any endorsement.