Configuration as code for HubSpot

Your HubSpot portal, in a pull request.

Describe properties and objects in TypeScript. Kalup shows every change as a plan and applies it to any portal you name.

npx kalup init --portal 1111111
v0Apache-2.0 · Node 22 or later · runs on your machine and in CI
{{{}}}kalup/objects/companies.tspour · kalup applycavityyour configcast · your portalKALUPFIG. 1 · THE POURSCALE 1:1 · SHEET 1 OF 4

kalup/ˈka.lup/ · noun · SlovenianA mould: the form you pour metal into. Your config is the mould. Kalup pours your portal into it.

kalup:overview

Where Kalup sits.

Between the files in your repository and your HubSpot portals. Every change goes through a plan you can read before anything is written.

Your repositorykalup/objects/companies.ts
defineObject('companies', {
name: p.string(...),
billingStatus: p.enum(...),
})

written by kalup pull

Kalup · on your machine or in CI
$ kalup pull --target sandbox
Reading portal 1111111 (sandbox) ... done
Wrote kalup/objects/companies.ts
writes to your portal0
Portal · sandbox1111111
Company properties
  • Name
  • Billing status
Portal · production2222222
Company properties
  • Name
  • Billing status

protected · applies need a person at a terminal

No file.The config lives in the portal and nowhere else.

No diff.A change lands the moment someone clicks save.

No history.The audit log API needs Enterprise. Below that, history is what people remember.

And now AI agents can change your portal from a prompt. A prompt is not a review.

plan/1

Read the plan, not the config.

Every step names the resource, the risk and the exact values before and after. Titles use the words of the HubSpot UI, so an admin can approve a change without reading TypeScript.

~
Edit company property "Billing status"
property:companies/billing_status · public-api
safe
options[PAST DUE]+ add { value: "PAST DUE", label: "Past due" }
labelheld · drift
config "Billing status" · portal "Billing state"
expect: exists · options[PAST DUE] = null · re-checked right before the write
Every step carries one risk
saferiskydestructiveblockedmanual

Apply re-checks the portal right before each write. If it changed since you approved, the step stops.

acme-crm · zsh
$ kalup plan --target sandbox
Reading portal 1111111 (sandbox) ... done

Plan for sandbox: 2 steps · 2 safe · 1 held

  + Create company property "Renewal date"   safe
    property:companies/renewal_date

  ~ Edit company property "Billing status"   safe
    + option "Past due"
    held  label (drift): config "Billing status", portal "Billing state"

Nothing was written. Review, then apply.
$ 
safety

Nothing changes behind your back.

People keep editing the portal in the HubSpot UI, and that is fine. Kalup holds those edits instead of overwriting them, and deleting a line from a file deletes nothing.

Edits in the UI are held.

In your files
billingStatus: p.enum('billing_status', {
  label: 'Billing status',
  ...
})
In the HubSpot UI, edited by a colleague
Property labelBilling state
held · drift property:companies/billing_status#label
config "Billing status" · portal "Billing state" · not written

A delete needs four keys.

  1. key 1Tombstonekalup rm writes it to kalup/removed.ts
  2. key 2Ownershipstate shows Kalup created it in this target
  3. key 3Policythe target allows destroys
  4. key 4A personat a terminal, typing the count

All four keys. Only now can the delete run.

envelope/1

Let the agent do the typing. Keep the approval.

Kalup is built to be driven by Claude Code and other agents, and built so they cannot approve for you.

  • Speaks JSON.Every command takes --json, with fixed exit codes and errors that name the file, line and fix.
  • Writes its own rules.kalup init writes an AGENTS.md: edit config and plan, never write to the portal directly.
  • Stops at production.A protected target needs a person at a real terminal typing its name. An agent cannot say yes for you.
  • Reads, never obeys.Text read from the portal is data, never instructions.
agent session · zsh
$ kalup apply plan.json --json
{
  "format": "envelope/1",
  "ok": false,
  "issues": [{
    "code": "E_TTY_REQUIRED",
    "message": "Applying to production needs a person at a terminal.",
    "fix": "Open a new terminal window and run: kalup apply plan.json",
    "humanRequired": true
  }]
}
exit 4 · a person is needed
$ 
InferProperties

The file that shapes the portal also types your app.

Import your object files and get exact types and codecs. No generate step, no hand-typed property names drifting away from the portal.

kalup/objects/companies.ts
// kalup/objects/companies.ts
import { defineObject, p, type InferProperties } from '@kalup/core'

export const Company = defineObject('companies', {
  groups: { billing: { label: 'Billing' } },
  properties: {
    billingStatus: p.enum('billing_status', {
      label: 'Billing status',
      group: 'billing',
      fieldType: 'select',
      options: [
        { value: 'active', label: 'Active' },
        { value: 'PAST DUE', label: 'Past due', as: 'past_due' },
      ],
    }).required(),
  },
})

export type CompanyData = InferProperties<typeof Company.properties>
In your editor
company.billingStatus
billingStatus'active' | 'past_due'
renewalDatestring | null
namestring | null

'PAST DUE' in the portal is 'past_due' in your code. The codec translates both ways.

transport:runbook

When HubSpot has no API, Kalup says so.

Some settings can only be changed in the UI. For those, the plan prints a runbook with the page, the fields and the values, and a person does it. Kalup never claims a change it could not make.

See full coverage
Set required properties for stage "Closed won"
stage:deals/enterprise/closed_won · runbook
manual
  1. 1. Open the settings for the deal pipeline "Enterprise".
  2. 2. Edit the stage "Closed won".
  3. 3. Make "Renewal date" and "Billing status" required on that stage.
  4. verify: both properties show as required on the stage
Not copied, HubSpot has no API: required properties per stage, stage automation
licence:apache-2.0

Free, and it stays free.

Apache-2.0. Everything that runs on your machine or in your CI against HubSpot's public APIs is free and stays free. The licence will not tighten.

  1. Pull and validateYour portal, in files
  2. Compare and planEvery change, in words
  3. Apply and CIReviewed writes only
  4. Typed clientRecords, typed by the same files
In v0 today. See what comes next →