Runtime HTTP API¶
hsemulator exposes a minimal, engine-backed HTTP runtime for validating and executing HubSpot custom code actions.
The runtime is intentionally thin.
All logic, validation rules, execution semantics, and determinism guarantees live in the engine, not the API layer. The HTTP API is a transport and orchestration surface only.
Purpose of the Runtime¶
The HTTP runtime exists to:
Validate action configurations safely
Execute actions deterministically
Support UI-driven and remote execution
Act as a control-plane target for orchestration
It does not introduce new behaviour beyond the CLI.
Philosophy¶
The runtime follows these principles:
Single execution engine
No duplicated logic
Validation is mandatory
No implicit execution
No hidden defaults
Inline and filesystem execution behave identically
CLI, CI, and HTTP are behaviourally equivalent
If a configuration is invalid locally, it is invalid over HTTP.
Starting the Runtime¶
Start the runtime server with:
hsemulate runtime
Specify a listen address:
hsemulate runtime --listen 0.0.0.0:8080
Default address:
http://127.0.0.1:8080
Authentication¶
All endpoints except /health are protected by API key authentication.
Header¶
Authorization: Bearer <API_KEY>
Requests without a valid API key are rejected.
Authentication is enforced via middleware before request handling.
Endpoints Overview¶
Endpoint |
Purpose |
|---|---|
|
Liveness probe |
|
Validate filesystem config only |
|
Validate + execute inline config |
GET /health¶
Liveness probe.
Request¶
GET /health
Response¶
ok
No authentication required
No checks performed
Intended for probes and load balancers only
POST /validate¶
Validate a filesystem-based config without executing any code.
This endpoint performs static validation only:
Schema correctness
Required fields
Action entry existence
Fixture existence and JSON validity
Runtime configuration
Budget sanity checks
No runtimes are spawned.
Request Body¶
/validate accepts a filesystem-backed config object.
{
"version": 1,
"action": {
"type": "js",
"entry": "actions/action.js"
},
"fixtures": [
"fixtures/event.json"
],
"env": {
"HUBSPOT_TOKEN": "pat-test-token",
"HUBSPOT_BASE_URL": "https://api.hubapi.com"
},
"runtime": {
"node": "node",
"python": "python"
},
"snapshots": {
"enabled": true
},
"repeat": 1
}
Notes:
Paths are resolved relative to the runtime working directory
No
modefield is acceptedNo execution occurs
Successful Validation Response¶
{
"execution_id": "exec_abc123",
"mode": "validate",
"valid": true,
"errors": []
}
Validation Failure Response¶
{
"execution_id": "exec_abc123",
"mode": "validate",
"valid": false,
"errors": [
{
"code": "ACTION_NOT_FOUND",
"message": "Action entry does not exist: actions/action.js"
}
]
}
Validation errors are:
Deterministic
Structured
Stable across CLI, CI, and HTTP
POST /execute¶
Validate and execute an inline configuration.
This is the canonical execution API.
Execution always runs validation first.
Request Body¶
/execute accepts an inline execution request.
{
"mode": "execute",
"config": {
"version": 1,
"action": {
"language": "js",
"entry": "actions/action.js",
"source": "exports.main = async (event) => { return { ok: true }; }"
},
"fixtures": [
{
"name": "fixtures/event.json",
"source": "{ \"input\": \"hello\" }"
}
],
"env": {
"HUBSPOT_TOKEN": "pat-test-token",
"HUBSPOT_BASE_URL": "https://api.hubapi.com"
},
"runtime": {
"node": "node",
"python": "python"
},
"snapshots": {
"enabled": true
},
"repeat": 1
}
}
Inline Execution Model¶
For /execute:
Inline config is validated (no filesystem access)
A temporary workspace is created
Action and fixtures are materialised
Execution runs via the same engine as CLI
All events are collected and returned
There is no behavioural difference between inline and filesystem execution.
Execution Modes¶
Mode |
Behaviour |
|---|---|
|
Validate + execute |
|
Validate only (dry-run) |
If mode is omitted, it defaults to execute.
Dry-Run Example¶
{
"mode": "validate",
"config": { ... }
}
Performs full inline validation
Does not execute code
No runtimes spawned
Successful Execution Response¶
{
"summary": {
"status": "executed",
"execution_id": "exec_abc123",
"duration_ms": 42
},
"events": [
{
"kind": "execution_created"
},
{
"kind": "validation_started"
},
{
"kind": "execution_started"
},
{
"kind": "stdout",
"data": "..."
},
{
"kind": "execution_completed"
}
]
}
Validation Failure via /execute¶
If validation fails, execution is short-circuited:
{
"summary": {
"status": "validation_failed",
"execution_id": "exec_abc123"
},
"events": [
{
"kind": "validation_failed"
}
]
}
No code is executed.
Promotion (/promote)¶
The /promote endpoint updates an existing HubSpot CUSTOM_CODE workflow action with tested source code.
It is designed to be:
Deterministic
Safe by default
Fully automatable from CI or a UI
Compatible with
hsemulate test+ snapshot gating
Promotion does not create workflows or actions. It updates an existing action in place.
What Promotion Does¶
A promotion performs the following steps:
Receives raw action source code (JS or Python)
Computes a canonical SHA-256 hash of the source
Injects a
hsemulator-shamarker commentFetches the target HubSpot workflow
Locates the target
CUSTOM_CODEaction by selectorApplies drift protection
Updates the action source (and runtime if provided)
Writes the updated workflow back to HubSpot
All steps are atomic from the caller’s perspective.
Drift Protection¶
Promotion uses a hash marker to ensure ownership and prevent accidental overwrites.
Injected marker (example):
// hsemulator-sha: a3f4c1...
or (Python):
# hsemulator-sha: a3f4c1...
Rules:
If the existing action has the same hash → no-op
If the existing hash differs → update proceeds
If no marker exists:
Promotion fails by default
force: trueoverrides this protection
This prevents overwriting manually-edited or externally-managed actions.
Endpoint¶
POST /promote
This endpoint is protected by the runtime API key middleware.
Request Body¶
{
"hubspot_token": "pat-xxxx",
"workflow_id": "123456789",
"selector": {
"type": "secret",
"value": "HUBSPOT_PRIVATE_APP_TOKEN"
},
"runtime": "nodejs18.x",
"source_code": "// action source here",
"force": false,
"dry_run": false
}
Request Fields¶
hubspot_token (required)¶
HubSpot private app token used to fetch and update the workflow.
Must belong to the portal containing the workflow
Must have automation/workflow scopes
This token is not stored by the runtime.
workflow_id (required)¶
The HubSpot workflow ID containing the target action.
This must be a valid workflow accessible by the token.
selector (required)¶
Identifies the CUSTOM_CODE action to update.
Currently supported selector:
{
"type": "secret",
"value": "HUBSPOT_PRIVATE_APP_TOKEN"
}
This matches against action.secretNames[].
Rules:
Exactly one matching action must be found
Multiple matches cause failure
Zero matches cause failure
runtime (optional)¶
Overrides the action runtime in HubSpot.
Example values:
nodejs18.xpython3.11
If omitted, the existing runtime is preserved.
source_code (required)¶
Raw JavaScript or Python source code to promote.
Hash marker is injected automatically
Existing marker is replaced if present
force (optional, default false)¶
Overrides safety checks.
Effects:
Allows overwriting actions without a hash marker
Skips drift ownership protection
Use with caution.
dry_run (optional, default false)¶
If true:
No PUT request is sent to HubSpot
Full validation and hashing still occur
A summary response is returned
Recommended for CI validation and previews.
Responses¶
Success (Dry Run)¶
{
"ok": true,
"dry_run": true,
"workflow_id": "123456789",
"hash": "a3f4c1...",
"action_index": 4
}
Success (Promotion Applied)¶
{
"ok": true,
"workflow_id": "123456789",
"hash": "a3f4c1...",
"revision_id": "987654321"
}
No-Op (Already Up To Date)¶
{
"ok": true,
"status": "noop",
"hash": "a3f4c1..."
}
Failure Examples¶
Unauthorized (API key):
{
"ok": false,
"error": "Unauthorized"
}
Invalid selector:
{
"ok": false,
"error": "Only selector.type = 'secret' is supported"
}
Workflow fetch failure:
{
"ok": false,
"error": "HubSpot GET flow failed: 400 Bad Request Invalid request"
}
Interaction With Tests and Snapshots¶
The runtime /promote endpoint itself does not execute tests.
However, it is designed to be called only after:
hsemulate testSnapshot comparison
Budget enforcement
Assertion validation
The CLI hsemulate promote command enforces these gates automatically.
When using /promote directly, the caller is responsible for enforcing test discipline.
Safety Guarantees¶
No workflow creation
No action creation
Deterministic selection
Ownership protection via hash
Explicit override required for unsafe writes
Promotion is intentionally strict.
Relationship Between /validate and /execute¶
Endpoint |
Behaviour |
|---|---|
|
Filesystem config validation |
|
Canonical inline dry-run |
|
Validate + execute inline |
Both paths share the same validation and execution engine.
Error Handling¶
The runtime returns:
200 OKfor successful validation or execution400 Bad Requestfor invalid configs or execution errors401 Unauthorizedfor missing or invalid API keys
Errors are structured and machine-readable.
Determinism Guarantees¶
The runtime guarantees:
Validation precedes execution
No execution during dry-run
No implicit retries
No hidden side effects
Identical behaviour across CLI, CI, and HTTP
When to Use the Runtime¶
The HTTP runtime is intended for:
UI-driven execution
Control-plane orchestration
Managed runners
Preflight validation
Remote execution environments
It is not intended to replace the CLI for local development.
Best Practices¶
Use
/validatefor filesystem-based preflight checksUse
/execute { mode: validate }for inline validationTreat
/executeas the canonical execution APINever bypass validation
Never rely on implicit defaults