Skip to content

Compose your own

Write the steps once and call them by one name: your API, CLI, SDK and MCP get the call, priced before it runs.

Writes
workflow() and Service()
Deploy computes
The kind and the price
Tests
Free, against test fixtures
SDK
startup-builder@0.3

One file becomes one call

verb publishes the file as a Verb on a hub Noun in your own namespace. input becomes its schema, maxCost its cap, and trigger when it runs on its own. It may not shadow a platform Verb: Audience.build is taken, so this file names its call Audience.buildHiring.

TypeScript
// workflows/hiring-brokers.ts: Leaseline's own weekly list of property managers who just hired
import { $, workflow } from 'startup-builder'

export default workflow({
  name: 'hiring-brokers',
  verb: 'Audience.buildHiring',                  // a new Verb on a hub Noun, in your namespace
  input: { roles: 'string[]', size: 'integer' },
  trigger: { schedule: 'mon 07:00' },
  maxCost: 3,
  execute: async (ctx, { roles = ['leasing coordinator'], size = 50 }) => {
    const hiring = await ctx.step('hiring', () => $.Organization.find({ icp: 'icps/icp_4', where: { hiringFor: roles }, limit: size * 2, maxCost: 0.1 }))
    return ctx.step('list', () => $.Audience.build({ icp: 'icps/icp_4', titles: ['owner'], size, from: hiring.organizations, maxCost: 2.5 }))
  },
})
Terminal
npx startup-builder dev                         # previews the new Verb as a Change, with its quote
ctx methodWhat it does
ctx.step(name, fn)Runs one call exactly once, keyed by name; a retry replays its result and never charges twice
ctx.sleep(duration)Waits, free: ctx.sleep('1d')
ctx.waitForEvent(name, { where, timeout })Waits for an Event, such as a reply, free
ctx.ask(question)Puts one question to a person, before money moves
ctx.approve(what)Waits on an Approval: only where you set an approval rule, since send and publish need none by default

Deploy computes the kind and the price

The kind is the promise, read from two fields, never from whether a step is code or agentic. The price is the sum of each step’s maxCost, capped by the file’s own, so try quotes it before it runs.

The file hasDeploy makes itDeploy refuses with
Neither grade nor contractA Workflow: the tool works/errors/uncapped-step when a step has no maxCost
grade: { evaluator, passWhen, remedy }An agent task: each output passes its grade, or is redone or free/errors/uncapped-step
contract: { done, accept, assurance, remedy, earns }A Service: the outcome, warranted/errors/service-needs-contract when review or oversight is missing, or contract.done names no Event
Both grade and contractRefused: a Service warrants its outcome under its contract, so its grade is its review/errors/service-needs-contract

Add a grade and it becomes an agent task

A grade is an Evaluator’s verdict on each output before it counts, and what happens on a fail: redo or free. The Evaluator runs on another model than the step it grades. This file answers one problem post as you, and follows the reply.

TypeScript
// workflows/answer-the-asker.ts: one problem post in, one graded answer posted as you, and the reply followed
import { $, workflow } from 'startup-builder'

export default workflow({
  name: 'answer-the-asker',
  verb: 'Post.answerAsker',
  input: { thread: 'string' },
  trigger: { event: 'Keyword.searched', where: "change = 'problem-question'" },   // a Monitor on Keyword.search
  maxCost: 0.3,
  grade: {
    evaluator: 'a second model reads the answer beside the thread, its best answers and the Offer',
    passWhen: 'it helps before it mentions anything; every claim holds; it reads like the best answers',
    remedy: 'redo',
  },
  execute: async (ctx, { thread }) => {
    const draft = await ctx.step('draft', () => $.Content.draft({ kind: 'reply', thread, from: 'offer', mention: 'when-asked', maxCost: 0.02 }))
    const reply = await ctx.step('post', () => $.Post.publish({ replyTo: thread, body: draft.body, maxCost: 0.25 }))   // graded before it posts
    const answer = await ctx.waitForEvent('Post.replied', { where: `thread = '${reply.thread}'`, timeout: '7d' })
    if (!answer) return { reply, outcome: 'no reply in 7 days' }
    return ctx.step('engage', () => $.Prospect.engage({ from: answer.$id }))         // a Lead, keeping this thread as its source
  },
})

Add a contract and it becomes a Service

A contract warrants the outcome: the Event that means done, who accepts it and how, the assurance, the remedy and the Event it bills on. A Service also names its review patterns and its oversight posture; a posture above none names its reason, and a capability gap is never one.

TypeScript
// workflows/comparison-pages.ts: a page per rival, published and indexed, warranted
import { $, Service } from 'startup-builder'

export default Service({
  name: 'comparison-pages',
  verb: 'Page.answerComparison',
  input: { rivals: 'string[]', site: 'string' },
  maxCost: 1,
  review: [
    { pattern: 'adversarial', rubric: 'every claim about a rival cites its own page or a public review' },
    { pattern: 'affirmative', rubric: 'a fair table first; who each product fits best' },
  ],
  oversight: { posture: 'none' },
  contract: {
    done: 'Content.published',                   // the Event that means done
    accept: { by: 'site-owner', within: '7d', silence: 'accept' },
    assurance: 'graded',
    remedy: 'redo',
    earns: 'Content.published',
  },
  execute: async (ctx, { rivals, site }) => {
    for (const rival of rivals) {
      await ctx.step(`page:${rival}`, () => $.Page.answer({ question: `Leaseline vs ${rival}`, site, maxCost: 0.3 }))
    }
  },
})

Extend a platform call with base()

To change a platform call, extend it. Name the steps you replace, and base() runs the rest unchanged, so a later fix to the platform call reaches yours.

TypeScript
import { $, workflow } from 'startup-builder'

export default workflow({
  name: 'audience-build-units',
  verb: 'Audience.buildByUnits',
  extends: 'Audience.build',
  maxCost: 3,
  steps: {
    // replace one named step; base() runs every other step of Audience.build unchanged
    companies: (ctx, input) => ctx.step('companies', () => $.Organization.find({ ...input, question: 'How many rental units do you manage?', maxCost: 0.8 })),
  },
  execute: async (ctx, input, base) => base(input),
})

Every call you write takes a goal

Deploy gives each call the ['code', 'agentic'] cascade: with every input set, the code runs; with { goal, budget }, the agent plans from your steps. Deploy asks for three to fifteen phrasings, its says, so a founder’s sentence finds it (goal mode).

FieldWhat deploy does with it
saysThe phrasings an agent matches a sentence against; without them, the call takes goals only through Goal.pursue
toolsYour step Verbs, which the agent may call more or fewer times than the file does
maxCostThe most one call spends; the goal’s budget sits above it

Test it for $0 against test fixtures

The SDK’s run runs the file with --env test: each data step returns its captured fixture, and an agentic step runs on recorded model output, so a test costs nothing and answers the same way each time.

TypeScript
import { run } from 'startup-builder'
import hiring from '../workflows/hiring-brokers'

const out = await run(hiring, { roles: ['leasing coordinator'], size: 10 }, { env: 'test' })
expect(out.cost.usd).toBe(0)                    // test fixtures answer every step for $0
expect(out.audience.size).toBe(10)

Start from a receipt, or let your agent propose it

A goal whose every change of course can be written as a rule offers “Make this a Workflow”: the plan’s steps become ctx.step calls and each Decision a declared rule. An agent that makes the same calls three times proposes the call itself: MCP do with define returns the draft and its quote, and deploying it waits for you unless a standing approval covers it.

JSON
{ "name": "do", "arguments": {
    "mode": "try",
    "define": { "verb": "Audience.buildHiring",
                "steps": [ { "call": "Organization.find", "input": { "where": { "hiringFor": ["leasing coordinator"] } } },
                           { "call": "Audience.build", "input": { "titles": ["owner"], "size": 50 } } ] } } }

Contents