Writing a JXA catalog entry and publishing it to the HUMΛN marketplace
The @human/connector-applescript catalog is the contract between HUMΛN and macOS apps. Built-in entries (builtin.*) cover Mail, OmniFocus, Calendar, and Reminders. Community entries (community.*) extend it for any scriptable app — Things 3, BBEdit, Reeder, Logic Pro, whatever your org uses.
This is the complete path from idea to marketplace.
Step 1: Understand the catalog shape
Every catalog entry is a JSON object:
{
"id": "community.acme.things3.project.add",
"tier": 2,
"app": "Things 3",
"description": "Add a project to Things 3",
"jxa_template": "var things = Application('Things 3'); things.projects.push(things.Project({ name: '{{name}}', notes: '{{notes}}' })); 'ok'",
"params_schema": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Project name" },
"notes": { "type": "string", "description": "Optional project notes", "default": "" }
},
"required": ["name"]
}
}
The fields:
| Field | Rules |
|---|---|
id |
community.<your-org>.<path> — must match [a-z][a-z0-9._-]+, never builtin.* |
tier |
0–3 (see tier model below — choose carefully, the intake agent validates) |
app |
Display name of the target macOS app |
description |
One sentence, plain English — shown in approval UI |
jxa_template |
JXA source with {{param}} placeholders — validated as non-empty |
params_schema |
JSON Schema for template params — validated as a JSON object |
Step 2: Choose the right tier
Tier assignment is the most important decision you make. The intake agent validates it and flags mismatches:
| Tier | What belongs here | Examples |
|---|---|---|
| 0 | Pure reads, no side effects, no app state change | jxa.ping, reading preferences |
| 1 | Non-destructive reads that open app connections | Read calendar events, list reminders |
| 2 | Mutations that create or change data | Add a task, draft an email, create a project |
| 3 | Irreversible actions | Send an email, delete a project, submit a form |
Anti-patterns:
- Tier 0 for a command that writes anything — the intake agent will flag it
- Tier 3 for a create (not irreversible) — overly conservative but wastes approval UX
- Tier 2 for send — the human must see a red "cannot undo" card for sends
Step 3: Write the JXA
JXA runs in the osascript -l JavaScript runtime. The connector pre-renders {{param}} placeholders before passing the source to osascript. Always return a string — stdout is what the muscle receives.
// Things 3 — add project
var things = Application('Things 3');
var proj = things.Project({
name: '{{name}}',
notes: '{{notes}}'
});
things.projects.push(proj);
JSON.stringify({ ok: true, name: '{{name}}' });
Safety rules enforced by the intake agent:
.keystroke(is blocked (PLATFORM:JXA_SAFETY_KEYSTROKE) — keystroke injection is never approveddoScriptis blocked (PLATFORM:JXA_SAFETY_DOSCRIPT) — arbitrary AppleScript execution bypasses the catalog tier model- The template must be static — no dynamic code generation in
jxa_template
Test it locally before publishing:
# Create a temp file and run it
cat > /tmp/test-things.js << 'EOF'
var things = Application('Things 3');
var proj = things.Project({ name: 'Test from HUMΛN', notes: 'Marketplace submission test' });
things.projects.push(proj);
JSON.stringify({ ok: true });
EOF
osascript -l JavaScript /tmp/test-things.js
# → {"ok":true}
If osascript reports "Not authorized to send Apple Events to Things 3", open System Settings > Privacy & Security > Automation and grant your terminal permission.
Step 4: Test against the connector
The connector validates your params schema before building the JXA:
import { buildJxaForCatalogCommand, mergeCatalogEntries } from '@human/connector-applescript';
// Register your community entry alongside the built-ins
mergeCatalogEntries([{
id: 'community.acme.things3.project.add',
tier: 2,
app: 'Things 3',
description: 'Add a project to Things 3',
jxa_template: "var things = Application('Things 3'); things.projects.push(things.Project({ name: '{{name}}', notes: '{{notes}}' })); 'ok'",
params_schema: {
type: 'object',
properties: {
name: { type: 'string' },
notes: { type: 'string', default: '' },
},
required: ['name'],
},
}]);
// Build JXA with params
const { jxa } = buildJxaForCatalogCommand('community.acme.things3.project.add', {
name: 'Q3 Planning',
notes: 'From the HUMΛN Companion',
});
console.log(jxa);
// → var things = Application('Things 3'); things.projects.push(things.Project({ name: 'Q3 Planning', notes: 'From the HUMΛN Companion' })); 'ok'
Step 5: Submit to the marketplace
Once the JXA is tested locally, submit via the API:
curl -X POST https://api.haio.run/v1/control-plane/marketplace/submit \
-H "Authorization: Bearer $HUMAN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"asset_type": "applescript_catalog_entry",
"name": "Things 3 — Add Project",
"description": "Add a project to Things 3 directly from the HUMΛN Companion or any macOS agent.",
"manifest_json": {
"catalog_entry": {
"id": "community.acme.things3.project.add",
"tier": 2,
"app": "Things 3",
"description": "Add a project to Things 3",
"jxa_template": "var things = Application('"'"'Things 3'"'"'); things.projects.push(things.Project({ name: '"'"'{{name}}'"'"', notes: '"'"'{{notes}}'"'"' })); '"'"'ok'"'"'",
"params_schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"notes": { "type": "string" }
},
"required": ["name"]
}
}
}
}'
The response includes a review_status: "pending". The marketplace intake agent runs:
- Structural validation — id format, tier range, jxa_template non-empty, params_schema is an object
- JXA safety flags —
.keystroke,doScript→ automatic rejection - Mission scan — checks for exclusion language, surveillance patterns, monetisation of personal data (same checks as connectors and extensions)
- Risk classification —
low | medium | highbased on all violations
Typical turnaround for tier 0–2 community entries with clean JXA: hours, not days.
Step 6: Install in your org
Once approved, install the catalog entry via the Console or API:
curl -X POST https://api.haio.run/v1/orgs/$ORG_DID/marketplace/install \
-H "Authorization: Bearer $HUMAN_TOKEN" \
-d '{ "asset_id": "your-entry-asset-id" }'
After installation, mergeCatalogEntries is called at connector load time with the org's approved community entries. Any muscle or agent calling bridge.invoke with your command ID will now resolve it.
What the muscle does with it
When your community.acme.things3.project.add command is invoked via the muscle:
- Muscle validates
tier: 2→ callsctx.approval.request()→ Workforce inbox - Reviewer sees "Add a project to Things 3" with params
{ name: 'Q3 Planning', notes: '...' } - On approval:
ctx.call.invoke('applescript', { action: 'bridge.invoke', command_id: '...', arguments: { name, notes } }) - On Path A: connector builds JXA, runs it, returns
{ ok: true } - On Path B: muscle returns
MacAutomationIntent→ Companion renders tier-amber card - In both cases:
ctx.provenance.log({ action: 'mac.invoke.executed', status: 'success', metadata: { command_id, tier } })
Your community command has the same governance, the same HITL gate, and the same provenance trail as any built-in command. The catalog is just the data — the muscle provides the policy.
Mac Automation Reference — Part 2 of 3