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

@fabianferno/portalflow-sdk

v0.1.0

Published

Typed REST client + run helpers + workflow-as-code builder for PortalFlow (visual on-chain automation for Portaldot).

Readme

@portalflow/sdk

Typed REST client + run helpers + a workflow-as-code builder for PortalFlow — visual on-chain automation for Portaldot (Substrate; token POT, SS58 prefix 42, 14 decimals).

  • Typed REST client — every PortalFlow API route, typed, with a configurable base URL and pluggable auth.
  • Run helpersrun() / test() / runAndWait() over the synchronous execution endpoints, with a typed step trace.
  • Workflow-as-code builder — assemble valid workflow graphs fluently. The per-node methods and config/output types are generated from the live node catalog (GET /node-types), so the SDK stays in lockstep with the backend executors.

Node 18+ (uses global fetch). Browser-safe — no Node-only APIs on the client path.

npm install @portalflow/sdk

Quick start

import { PortalFlowClient } from "@portalflow/sdk";

const pf = new PortalFlowClient({
  baseUrl: "http://localhost:8000/api/v1", // default
  token: process.env.PORTALFLOW_TOKEN,      // JWT (Bearer) or the shared API_KEY
});

// Or log in (stores the token on the client):
await pf.auth.login("[email protected]", "password");

const { items } = await pf.workflows.list({ status: "Active", trigger: "manual" });
const result = await pf.workflows.run(items[0].id); // { run, steps }
for (const s of result.steps) {
  console.log(s.node_type, s.status, s.fee, s.extrinsicHash);
}

Auth

The SDK attaches whatever token you give it as Authorization: Bearer <token>. That can be a JWT from auth.login / auth.signup, or the shared API_KEY (which the backend maps to owner_id="default"). Full session/login management is out of scope; pass a token you already have, or call login.

pf.setToken(jwt);          // attach a token you already hold
const me = await pf.auth.me();

REST client

// Workflows
await pf.workflows.list({ status: "Active", trigger: "manual", q: "faucet" });
await pf.workflows.get(id);
await pf.workflows.create({ name, desc, trigger, tags, graph });
await pf.workflows.update(id, { graph });
await pf.workflows.setStatus(id, "Active");
await pf.workflows.duplicate(id);
await pf.workflows.remove(id);

// Runs / catalog / wallet / templates
await pf.runs.list({ status: "Success", page: 1, perPage: 50 });
await pf.runs.get(runId);
await pf.nodeTypes();          // public, no auth — the executor-backed catalog
await pf.templates.list({ cat, q });
await pf.wallet.get();

Any non-2xx response throws a PortalFlowError (status, code, message, path). GET requests are retried on network/5xx errors with bounded backoff; non-idempotent calls (/run, /wallet/send, …) are never retried.

Run helpers

/run (live — signs + submits) and /test (dry-run) are synchronous: they block until the whole graph finishes and return { run, steps }.

const result = await pf.workflows.run(id);                       // live
const dry    = await pf.workflows.test(id);                      // dry-run
const final  = await pf.workflows.runAndWait(id, { refetch: true, timeoutMs: 60_000 });

runAndWait returns the synchronous run result; with refetch: true it re-reads GET /runs/{id} for the canonical persisted record. There is no server-side streaming, so there is no live step-by-step stream in v1.

Workflow-as-code

import { workflow, addr, pot } from "@portalflow/sdk";

const wf = workflow("Auto top-up")
  .trigger.manual()
  .get_account({ account: addr("5Grw…") })
  .condition({ left: "x", op: "greater than", right: pot(5) })
  .build();

const created = await pf.workflows.create({
  name: wf.name,
  trigger: wf.trigger,
  graph: wf.graph,
});
  • Chaining = an edge from the previous node to the new one. .build() emits { nodes, edges } with auto-assigned x/y layout and idle status.
  • Branch ports: condition / ai_gate expose .true / .false; switch exposes .case(i). Selecting a branch routes the next edge through that port.
  • Titles & ids are generated and deduped (GetAccountInfo, GetAccountInfo2).

Branches and typed refs

A node method returns a handle — keep chaining, or capture it to branch and to build typed {{Title.output.field}} references:

const wf = workflow("balance guard");
wf.trigger.manual();
const acct = wf.get_account({ account: addr("5Grw…") });

const cond = wf.condition({ left: acct.ref("free_balance"), op: "less than", right: pot(5) });
cond.true.pot_transfer({ to: addr("5abc…"), amount: pot(5) });
cond.false.stop({ message: "balance ok" });

const built = wf.build();
// acct.ref("free_balance")  ->  "{{GetAccountInfo.output.free_balance}}"

ref(field) is constrained to the node's generated output union, so a typo or a renamed output is a compile error.

Validation

.build() throws PortalFlowBuildError when the graph is invalid: no/multiple trigger nodes, dangling edges, a branch port on a non-branch node, or a missing required field (the offending node id/field is on the error).

Round-trip

const existing = await pf.workflows.get(id);
const wf = WorkflowBuilder.fromGraph(existing.name, existing.graph)
  .telegram({ token: "…", chatId: "…", text: "appended step" })
  .build();
await pf.workflows.update(id, { graph: wf.graph });

Known limitations

  • Triggering is manual-only on the backend. /run is the only operational trigger. schedule / webhook / on-chain trigger nodes exist in the catalog but have no backend listener — the SDK can create such workflows but fires them by calling /run; automatic firing is separate backend work.
  • Assets/staking writes degrade to Skipped on the dev node's V13 metadata (BadProof). The builder will happily emit those nodes, but runs report Skipped for them — typed success ≠ on-chain success for those types.
  • The bundled catalog is a snapshot (CATALOG_VERSION). The package ships a drift-guard test; compare against the live catalog with pf.nodeTypes() and regenerate (npm run codegen) when the backend catalog changes.

License

MIT