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

@headlessly/platform

v0.1.2

Published

headless.ly Platform entities — Workflow, Integration, Agent

Readme

@headlessly/platform

Zapier connects disconnected systems with a visual canvas. Your agent doesn't need a canvas — the systems aren't disconnected.

import { Workflow, Agent } from '@headlessly/platform'

await Workflow.create({
  name: 'New Customer Onboarding',
  trigger: 'Deal.closed',
  steps: [
    { action: 'Subscription.create', params: { plan: 'pro' } },
    { action: 'Ticket.create', params: { subject: 'Welcome aboard' } },
    { action: 'Campaign.create', params: { type: 'Email', name: 'Onboarding' } },
  ],
})

// A deal closes — the workflow receives the real Deal, not a webhook payload
Workflow.activated(async (workflow, $) => {
  await $.Agent.deploy('agent_k7TmPvQx')
  await $.Event.create({ type: 'workflow.activated', value: workflow.name })
})

No Zapier zaps. No Make scenarios. No n8n node wiring. Workflows operate on real typed entities in the same graph as your CRM, billing, support, and analytics. Your agent orchestrates the business — not webhook payloads.

The Problem

Zapier has 7,000 integrations because it needs 7,000 integrations — every app is a silo that needs a bridge. Make lets you build "scenarios" that visually wire HubSpot to Stripe to Zendesk through HTTP requests and JSON transformers. n8n is self-hosted Make with more YAML.

None of them were built for an AI agent to operate.

Your agent doesn't need a drag-and-drop canvas. It needs Workflow.activate(). It doesn't need a trigger/action visual builder with 47 authentication screens. It needs typed entities that are already connected. It doesn't need to parse a Stripe webhook payload, map it to a HubSpot contact schema, and POST it to a Zendesk API. It needs a BEFORE hook:

Workflow.activating((workflow) => {
  if (workflow.steps.length === 0) throw new Error('Cannot activate empty workflow')
})

The real problem with Zapier isn't Zapier — it's that your CRM, billing, and support are three different systems. When they're the same graph, you don't need 7,000 integrations. You need zero.

One Typed Graph

When a workflow triggers in Zapier, it receives a webhook payload — untyped JSON you pray hasn't changed since last week. You parse it, transform it, map fields, handle errors, and hope the next step's API is still accepting the same schema.

In headless.ly, a workflow triggered by Deal.closed receives the actual Deal — with its contact, organization, subscription, and every relationship already resolved:

import { Workflow } from '@headlessly/platform'
import { Deal } from '@headlessly/crm'

Deal.closed(async (deal, $) => {
  await $.Subscription.create({ plan: 'pro', customer: deal.contact })
  await $.Ticket.create({ subject: `Welcome ${deal.name}`, requester: deal.contact })
  await $.Campaign.create({ name: `Onboard ${deal.name}`, type: 'Email' })
  await $.Event.create({ type: 'deal.closed', value: deal.value })
})

Workflow.activated(async (workflow, $) => {
  await $.Agent.create({
    name: `${workflow.name} executor`,
    type: 'Workflow',
    model: 'claude-opus-4-6',
    systemPrompt: `Execute the steps in workflow ${workflow.$id}`,
  })
})

No webhook parsing. No field mapping. No Zapier. One graph.

Install

npm install @headlessly/platform

Entities

Workflow

Event-driven automation that operates on real entities — not webhook payloads. A workflow triggered by Contact.qualified receives the actual Contact with its relationships, not a JSON blob.

import { Workflow } from '@headlessly/platform'

const workflow = await Workflow.create({
  name: 'Lead Qualification Pipeline',
  trigger: 'Contact.created',
  steps: [
    { action: 'Contact.qualify', condition: 'leadScore > 80' },
    { action: 'Deal.create', params: { stage: 'Qualification' } },
    { action: 'Agent.deploy', params: { type: 'Specialist' } },
  ],
  errorHandling: 'Continue',
  timeout: 30000,
})

await Workflow.activate(workflow.$id)
await Workflow.pause('workflow_fX9bL5nRd')
await Workflow.archive('workflow_fX9bL5nRd')

Workflow.activated((workflow) => {
  console.log(`"${workflow.name}" is live — trigger: ${workflow.trigger}`)
})

Verbs: activate() · activating() · activated() · activatedBy · pause() · pausing() · paused() · pausedBy · archive() · archiving() · archived() · archivedBy

Key fields: name, trigger, steps, retryPolicy, errorHandling (Stop | Continue | Fallback), timeout, status (Draft | Active | Paused | Archived), lastRunAt, runCount, successCount, failureCount

Relationships: -> Organization, <- Agents[]

Integration

External systems connected into the graph — not bolted on through middleware. When Stripe connects as an integration, its data becomes native entities in the same graph.

import { Integration } from '@headlessly/platform'

const stripe = await Integration.create({
  name: 'Stripe',
  slug: 'stripe',
  provider: 'stripe',
  category: 'Payment',
  authType: 'OAuth2',
  status: 'Available',
})

await Integration.connect(stripe.$id)
await Integration.disconnect('integration_k7TmPvQx')

Integration.connected((integration) => {
  console.log(`${integration.name} connected via ${integration.authType}`)
})

Verbs: connect() · connecting() · connected() · connectedBy · disconnect() · disconnecting() · disconnected() · disconnectedBy

Key fields: name, slug, provider, providerUrl, category (Payment | CRM | Marketing | Analytics | Communication | Storage | AI | Other), authType (OAuth2 | ApiKey | Basic | Custom), status (Available | ComingSoon | Deprecated), oauthScopes, configSchema, webhookSupport

Agent

First-class entities in the graph — not black-box API calls. Agents can be deployed, paused, retired, monitored, and composed into workflows. They have memory, tools, and a full lifecycle.

import { Agent } from '@headlessly/platform'

const agent = await Agent.create({
  name: 'Sales Qualifier',
  type: 'Specialist',
  model: 'claude-opus-4-6',
  systemPrompt: 'Qualify inbound leads by analyzing company fit and engagement signals.',
  tools: ['search', 'fetch', 'do'],
  memory: 'Persistent',
  visibility: 'Team',
})

await Agent.deploy(agent.$id)
await Agent.pause('agent_k7TmPvQx')
await Agent.retire('agent_k7TmPvQx')

Agent.deployed(async (agent, $) => {
  await $.Event.create({ type: 'agent.deployed', value: agent.name })
  await $.Workflow.create({
    name: `${agent.name} monitoring`,
    trigger: 'Agent.retired',
  })
})

Verbs: deploy() · deploying() · deployed() · deployedBy · pause() · pausing() · paused() · pausedBy · retire() · retiring() · retired() · retiredBy

Key fields: name, slug, model, systemPrompt, type (Assistant | Autonomous | Workflow | Specialist | Router), status (Draft | Active | Paused | Archived), visibility (Private | Team | Organization | Public), memory (None | Session | Persistent), tools, temperature, maxTokens, totalTokens, totalCost, successRate

Relationships: -> Organization, -> Owner (Contact), <- Workflows[]

Agent-Native

Your agent connects to one MCP endpoint. It can orchestrate your entire automation layer:

{ "type": "Workflow", "filter": { "status": "Active", "trigger": "Deal.closed" } }
{ "type": "Agent", "id": "agent_k7TmPvQx", "include": ["organization", "workflows"] }
const stale = await $.Workflow.find({ status: 'Active', lastRunAt: { $lt: '2026-01-01' } })
for (const workflow of stale) {
  await $.Workflow.pause(workflow.$id)
  await $.Event.create({ type: 'workflow.stale', value: workflow.name })
}

const idle = await $.Agent.find({ status: 'Active', successRate: { $lt: 0.5 } })
for (const agent of idle) {
  await $.Agent.retire(agent.$id)
  await $.Ticket.create({
    subject: `Low-performing agent: ${agent.name}`,
    priority: 'High',
  })
}

Three tools. Not seven thousand zaps.

Cross-Domain Operations

Query results are standard arrays — chain operations with familiar JavaScript:

const workflows = await Workflow.find({ status: 'Active' })
for (const workflow of workflows) {
  const agents = await Agent.find({ workflow: workflow.$id, status: 'Active', type: 'Specialist' })
  for (const agent of agents) {
    if (agent.successRate < 0.5) {
      await Agent.retire(agent.$id)
      await Ticket.create({ subject: `Low-performing agent: ${agent.name}`, priority: 'High' })
    }
  }
}

License

MIT