@nbt-dev/nbt 0.0.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/nbt.js +117 -36
  2. package/package.json +1 -1
  3. package/stdlib/auth/migrations/20260614000000_add_user_devtools_settings/migration.nbt +4 -0
  4. package/stdlib/auth/migrations/20260614000000_add_user_devtools_settings/schema_snapshot.nbt +59 -0
  5. package/stdlib/auth/schema.nbt +5 -30
  6. package/stdlib/calendar/schema.nbt +2 -3
  7. package/stdlib/crm/schema.nbt +3 -4
  8. package/stdlib/design/schema.nbt +2 -2
  9. package/stdlib/dns/schema.nbt +3 -3
  10. package/stdlib/email/schema.nbt +12 -11
  11. package/stdlib/ingest/schema.nbt +4 -4
  12. package/stdlib/notifications/schema.nbt +2 -2
  13. package/stdlib/phone/schema.nbt +9 -10
  14. package/stdlib/workflows/schema.nbt +69 -21
  15. package/vendor/linux-x64/cartridges/auth/migrations/20260614000000_add_user_devtools_settings/migration.nbt +4 -0
  16. package/vendor/linux-x64/cartridges/auth/migrations/20260614000000_add_user_devtools_settings/schema_snapshot.nbt +59 -0
  17. package/vendor/linux-x64/cartridges/auth/schema.nbt +5 -30
  18. package/vendor/linux-x64/cartridges/calendar/schema.nbt +2 -3
  19. package/vendor/linux-x64/cartridges/crm/schema.nbt +3 -4
  20. package/vendor/linux-x64/cartridges/design/schema.nbt +2 -2
  21. package/vendor/linux-x64/cartridges/dns/schema.nbt +3 -3
  22. package/vendor/linux-x64/cartridges/email/schema.nbt +12 -11
  23. package/vendor/linux-x64/cartridges/ingest/schema.nbt +4 -4
  24. package/vendor/linux-x64/cartridges/notifications/schema.nbt +2 -2
  25. package/vendor/linux-x64/cartridges/phone/schema.nbt +9 -10
  26. package/vendor/linux-x64/cartridges/workflows/schema.nbt +69 -21
  27. package/vendor/linux-x64/console +0 -0
  28. package/vendor/linux-x64/nbt +0 -0
  29. package/stdlib/crm/adapters/gohighlevel/README.md +0 -85
  30. package/stdlib/crm/adapters/gohighlevel/tests/README.md +0 -159
  31. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_138fields.json +0 -222
  32. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_140fields.json +0 -219
  33. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_alt.json +0 -212
  34. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_changed.json +0 -102
  35. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_created.json +0 -95
  36. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_full.json +0 -213
  37. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_sparse.json +0 -161
  38. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_update_a.json +0 -197
  39. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/contact_update_b.json +0 -197
  40. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/opportunity_changed.json +0 -85
  41. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/opportunity_created.json +0 -85
  42. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_contact_pilot.json +0 -43
  43. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_contact_with_price_closed.json +0 -7
  44. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_contact_with_price_open.json +0 -7
  45. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_appointment_delete.json +0 -1
  46. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_calendar_update.json +0 -1
  47. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_contact_create.json +0 -1
  48. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_event_opp_status_update.json +0 -1
  49. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_opportunity_pilot.json +0 -16
  50. package/stdlib/crm/adapters/gohighlevel/tests/fixtures/webhooks/v2_pipelines_pilot.json +0 -137
@@ -1,44 +1,92 @@
1
-
2
- # Question: How do we tie in cartridge metadata and workflow side effects into all this?
3
-
4
- enum WorkerStatus {
5
- WAITING
6
- RUNNING
7
- TERMINATED
8
- }
9
-
10
- entity Worker {
1
+ # An immutable, versioned workflow bundle (the JS artifact). Deploying new code
2
+ # registers a new version; in-flight executions keep replaying against the version
3
+ # they started on (the journal-header pin), so a code change can never corrupt a
4
+ # running instance. A version's source is retained while ≥1 execution pins it and
5
+ # dropped once the last drains (refcount drain-GC, in-memory). Durable so a pinned
6
+ # version survives restart.
7
+ entity WorkflowBundle {
11
8
  id: ulid
12
9
  createdAt: DateTime @default(now())
13
10
  updatedAt: DateTime @updatedAt
14
- source: blob
11
+ version: u32
12
+ source: string
15
13
  contentHash: string
16
- queue?: string
17
- # TODO: We need some sort of termination options like a restart policy
18
14
  }
19
15
 
20
- # Stub flesh out execution lifecycle fields later.
16
+ # A single durable workflow instance. The journal is the WorkflowExecutionEvent
17
+ # rows owned by this row (ordered by seq); status + result/error are the
18
+ # materialized terminal state. cursor = number of resolved host commands
19
+ # (== count of HOST_CALL_RESULT events) — the replay ordinal high-water mark.
21
20
  entity WorkflowExecution {
22
21
  id: ulid
23
22
  createdAt: DateTime @default(now())
24
23
  updatedAt: DateTime @updatedAt
25
- status: string
24
+ status: string # RUNNING (incl. async host call in flight) | SUSPENDED | COMPLETED | FAILED | CANCELLED
25
+ workflowName: string
26
+ args?: string # JSON arguments passed to runWorkflow
27
+ result?: string # JSON return value (COMPLETED)
28
+ error?: string # message + stack (FAILED)
29
+ cursor: u32
30
+ lane?: string # fairness lane; per-lane concurrency is capped so a burst of one lane can't starve others (default "default")
31
+ deployVersion: u32 # the bundle version this instance pins for its entire life (journal-header pin); replay always runs against this exact version
26
32
  events: WorkflowExecutionEvent[]
27
33
  }
28
34
 
29
- # Do we want this to be a string?
35
+ # A recurring trigger: fire `workflowName` whenever the cron expression matches.
36
+ # Idempotent per minute-bucket via a deterministic execution id (schedule + bucket),
37
+ # so a tick repeating within the same minute — or a restart mid-minute — never
38
+ # double-fires. Evaluated leader-only by the executor tick.
39
+ entity WorkflowSchedule {
40
+ id: ulid
41
+ createdAt: DateTime @default(now())
42
+ updatedAt: DateTime @updatedAt
43
+ workflowName: string
44
+ cron: string # 5-field: minute hour day-of-month month day-of-week
45
+ args?: string # JSON arguments passed to each fired execution
46
+ lane?: string # lane the fired executions run on
47
+ enabled: bool
48
+ lastFiredBucket: u32 # highest minute-bucket already fired; the durable idempotency watermark (no double-fire across ticks/restart)
49
+ }
50
+
51
+ # A durable record that a named event fired. Emissions ride the WAL (replicated
52
+ # like any row); the leader-only executor reconcile drains the unprocessed ones,
53
+ # looks up the event's triggers in the storage event registry, and fires each
54
+ # triggered workflow exactly once cluster-wide (only the leader drains; the fired
55
+ # id is derived from emission+index so a crash mid-drain re-fires idempotently).
56
+ entity WorkflowEvent {
57
+ id: ulid
58
+ createdAt: DateTime @default(now())
59
+ updatedAt: DateTime @updatedAt
60
+ event: string # "<slug>:<event>" — the event-registry key
61
+ payload?: string # JSON passed as args to each fired workflow
62
+ processed: bool # set true once the leader has fired this emission's triggers
63
+ }
64
+
30
65
  enum WorkflowExecutionEventKind {
31
- WORKFLOW_EXECUTION_STARTED
32
- WORKFLOW_STEP_SCHEDULED
33
- WORKFLOW_STEP_STARTED
34
- WORKFLOW_STEP_COMPLETED
35
- WORKFLOW_EXECUTION_COMPLETED
66
+ HOST_CALL_INTENT # recorded BEFORE a side effect is performed (durability)
67
+ HOST_CALL_RESULT # the resolved value of a host command (replay source)
68
+ SUSPENDED # parked awaiting an external command (e.g. a signal)
69
+ RESUMED # an external command arrived and re-drove the instance
70
+ COMPLETED # workflow function returned
71
+ FAILED # workflow function threw
72
+ CANCELLED # terminated by an explicit cancel request (cooperative)
73
+ RETRY # an async host call failed transiently and was retried (counter; payload = attempt)
74
+ CONTINUED # ended via continue-as-new; payload = the child execution id carrying the work forward
36
75
  }
37
76
 
77
+ # One journal entry. For HOST_CALL_* events seq is the command ordinal and
78
+ # (capability, target, op) is the descriptor; payload carries the args (INTENT)
79
+ # or the result JSON (RESULT). Lifecycle events (SUSPENDED/RESUMED/COMPLETED/
80
+ # FAILED/CANCELLED/RETRY) carry payload only.
38
81
  entity WorkflowExecutionEvent {
39
82
  id: ulid
40
83
  createdAt: DateTime @default(now())
41
84
  updatedAt: DateTime @updatedAt
42
85
  execution: WorkflowExecution @relation(onDelete: Cascade) # events are owned by their execution
86
+ seq: u32
43
87
  kind: WorkflowExecutionEventKind
88
+ capability?: string
89
+ target?: string
90
+ op?: string
91
+ payload?: string
44
92
  }
@@ -0,0 +1,4 @@
1
+ migration add_user_devtools_settings {
2
+ add_field User devtoolsSettings string default("")
3
+ set_optional User devtoolsSettings true
4
+ }
@@ -0,0 +1,59 @@
1
+ entity User {
2
+ name: string
3
+ username?: string
4
+ email?: string
5
+ emailVerified: bool
6
+ externalId?: string
7
+ capsVersion: u32
8
+ devtoolsSettings?: string
9
+ @@index([email])
10
+ @@index([externalId])
11
+ @@unique([email])
12
+ }
13
+
14
+ entity UserRole {
15
+ userId: string
16
+ cart: string
17
+ role: string
18
+ @@index([userId])
19
+ }
20
+
21
+ entity Session {
22
+ userId: string
23
+ token: string
24
+ expiresAt: DateTime
25
+ ipAddress?: string
26
+ userAgent?: string
27
+ @@index([token])
28
+ @@index([userId])
29
+ }
30
+
31
+ entity Account {
32
+ userId: string
33
+ providerId: string
34
+ password: string
35
+ accessToken: string
36
+ refreshToken: string
37
+ idToken: string
38
+ accessTokenExpiresAt: DateTime
39
+ refreshTokenExpiresAt: DateTime
40
+ scope: string
41
+ @@index([userId])
42
+ }
43
+
44
+ entity Verification {
45
+ identifier: string
46
+ value: string
47
+ expiresAt: DateTime
48
+ @@index([identifier])
49
+ }
50
+
51
+ entity ApiKey {
52
+ name: string
53
+ projectId: string
54
+ start: string
55
+ prefix: string
56
+ key: string
57
+ permissions: string
58
+ roles: string
59
+ }
@@ -1,6 +1,6 @@
1
1
  import crypto from "crypto"
2
2
 
3
- # Auth policy for the hand-written routes in native/runtime.jai (formerly the
3
+ # Auth policy for the hand-written routes in modules/cart/auth.jai (formerly the
4
4
  # @public actions + the `authenticate` middleware block). These keep the
5
5
  # daemon's public-route + middleware manifest correct: public routes bypass the
6
6
  # /api/* auth gate; @middleware tells the proxy this cart exports the global
@@ -25,7 +25,8 @@ export entity User {
25
25
  email?: string
26
26
  emailVerified: bool
27
27
  externalId?: string
28
- capsVersion: u32 # what is this?
28
+ capsVersion: u32 # capability version; bumped on every role change so cached authorizations invalidate
29
+ devtoolsSettings?: string # per-operator devtools UI preferences, stored as a JSON blob
29
30
 
30
31
  @@index([email])
31
32
  @@index([externalId])
@@ -101,8 +102,8 @@ entity ApiKey {
101
102
  }
102
103
 
103
104
  # Console-operator SSH public keys. The console daemon authenticates SSH
104
- # connections by looking up rows here via modules/auth_lookup/ (in-process,
105
- # direct storage read — auth cart liveness is not required).
105
+ # connections by reading these rows directly from storage (in-process
106
+ # auth cart liveness is not required).
106
107
  entity SshKey {
107
108
  id: ulid
108
109
  createdAt: DateTime @default(now())
@@ -114,29 +115,3 @@ entity SshKey {
114
115
  @@index([userId])
115
116
  @@unique([blob])
116
117
  }
117
-
118
- jai {
119
- find_synced_user :: (requestedId: string, email: string) -> (User, bool) {
120
- if requestedId.count > 0 {
121
- by_id, by_id_ok := get_user(requestedId);
122
- if by_id_ok return by_id, true;
123
-
124
- by_external := find_users_by_externalId(requestedId);
125
- if by_external.count > 0 {
126
- u, ok := get_user(by_external[0].id);
127
- if ok return u, true;
128
- }
129
- }
130
-
131
- if email.count > 0 {
132
- by_email := find_users_by_email(email);
133
- if by_email.count > 0 {
134
- u, ok := get_user(by_email[0].id);
135
- if ok return u, true;
136
- }
137
- }
138
-
139
- empty: User;
140
- return empty, false;
141
- }
142
- }
@@ -45,9 +45,8 @@ export entity Appointment {
45
45
 
46
46
  # Rendered embedding-template text. Persisted alongside the Appointment so
47
47
  # similarity search can score (target_text, candidate_text) pairs without
48
- # re-rendering the customer's @embed template on every query. Populated at
49
- # ingest by the customer's @embed binding
50
- # (D09.10c). Generic surface — every customer composing similarity search
48
+ # re-rendering on every query. Populated at ingest by the customer's
49
+ # pipeline. Generic surface — every customer composing similarity search
51
50
  # over Appointments needs it; the *content* of the text is customer policy.
52
51
  embedText?: string
53
52
 
@@ -77,11 +77,10 @@ export entity Deal {
77
77
  customData: document
78
78
 
79
79
  # Append-only suffix — fields below this comment must stay at the end
80
- # of the entity declaration. Codegen serializes in declaration order;
80
+ # of the entity declaration. The row codec serializes in declaration order;
81
81
  # newly-added fields go *after* existing fields so the on-disk byte
82
82
  # layout stays forward-compatible (old records deserialize cleanly with
83
- # trailing fields default-initialised). See the EOF-tolerant deserialize
84
- # codegen in modules/nbt/codegen_serial.jai.
83
+ # trailing fields default-initialised).
85
84
 
86
85
  # D09.02 — generic lifecycle status, customer-domain. mylocalpro reacts
87
86
  # to `deal.status == "won"` to mark the source Appointment positive.
@@ -101,7 +100,7 @@ export entity Deal {
101
100
 
102
101
  # Date the deal closed (won or lost). Resolved at GHL adapter ingest
103
102
  # from Contact custom field "Date Sold" via CONTACT_LATEST_OPEN_DEAL.
104
- # Append-only field — codegen serializes in declaration order, so
103
+ # Append-only field — the row codec serializes in declaration order, so
105
104
  # existing rows deserialize cleanly with this trailing default.
106
105
  closedDate?: DateTime
107
106
  }
@@ -136,5 +136,5 @@ entity Page {
136
136
  }
137
137
 
138
138
  # Behavior (former Design.create/save_version/get_head/list_versions actions +
139
- # tasks/parse.nbt @task) moved to native/runtime.jai over the generated ORM
140
- # see design_register_extra_routes.
139
+ # tasks/parse.nbt @task) moved to hand-written Jai over the generated ORM,
140
+ # which also registers the HTTP routes.
@@ -10,8 +10,8 @@
10
10
  # - Future: TLS cart, custom gateway-domains cart.
11
11
  #
12
12
  # Schema only. All behavior (token verify, CF API calls, DNS record CRUD,
13
- # TXT verify polling) lives in native/runtime.jai as regular Jai over the
14
- # generated ORM see domains_register_extra_routes.
13
+ # TXT verify polling) lives in hand-written Jai over the generated ORM, which
14
+ # also registers the HTTP routes.
15
15
 
16
16
 
17
17
 
@@ -21,7 +21,7 @@ entity DnsProvider {
21
21
  updatedAt: DateTime @updatedAt
22
22
  type: string # "cloudflare"
23
23
  label: string # user-facing name ("my CF account")
24
- apiToken: string # raw token (TODO: encrypt at rest — same status as phone cart's authToken)
24
+ apiToken: string # provider API token (stored as-is)
25
25
  accountId: string # CF account_id, needed for add_zone + registrar calls (optional)
26
26
  status: string # "ACTIVE" | "INVALID" | "DISCONNECTED"
27
27
  lastCheckedAt: u64 # ms epoch — updated on connect + test
@@ -1,7 +1,7 @@
1
1
  # Email cartridge — transactional send + synthetic inboxes.
2
2
  #
3
- # Provider: SendGrid (modules/sendgrid). Nothing in the user-facing surface
4
- # mentions SendGrid; the cart is branded "Email".
3
+ # Provider: SendGrid. Nothing in the user-facing surface mentions SendGrid;
4
+ # the cart is branded "Email".
5
5
  #
6
6
  # Per-console model:
7
7
  # - Every console auto-provisions `<console>.nbt.dev` as the default mail
@@ -13,21 +13,22 @@
13
13
  # register with SendGrid Inbound Parse + Domain Authentication, poll
14
14
  # until everything is green.
15
15
  #
16
- # Ops prerequisites (one-time, see plans/squishy-puzzling-spindle.md):
17
- # - SENDGRID_API_KEY in console env.
18
- # - SENDGRID_EVENT_PUBKEY in console env (base64 SPKI for event webhook verification).
16
+ # Ops prerequisites (one-time):
17
+ # - Provider API key in console env.
18
+ # - Provider event-webhook public key in console env (base64 SPKI for event
19
+ # webhook signature verification).
19
20
  # - `*.nbt.dev MX 10 mx.sendgrid.net` at zone level.
20
21
  # - SendGrid Inbound Parse wildcard entry + per-console entries (latter are
21
22
  # created programmatically by MailDomain.ensure_default).
22
23
 
23
- # Behavior (the former main.nbt actions) lives in native/runtime.jai over the
24
- # generated ORM see email_register_extra_routes. These imports trigger the
25
- # #load of the native files into the cart module scope.
24
+ # Behavior (the former main.nbt actions) lives in hand-written Jai over the
25
+ # generated ORM, which also registers the HTTP routes. The import below pulls
26
+ # the crypto module into the cart scope.
26
27
  import crypto from "crypto"
27
28
 
28
- # Auth policy for the hand-written routes (formerly @public actions). The
29
- # handlers live in native/runtime.jai; these keep the daemon's public-route
30
- # manifest correct so the /api/* auth gate is bypassed for SendGrid webhooks.
29
+ # Auth policy for the hand-written routes (formerly @public actions). These
30
+ # keep the daemon's public-route manifest correct so the /api/* auth gate is
31
+ # bypassed for SendGrid webhooks.
31
32
  @public_route "/api/email/inbound"
32
33
  @public_route "/api/email/events"
33
34
 
@@ -1,7 +1,7 @@
1
- # Behavior (the former main.nbt actions) lives in native/runtime.jai over the
2
- # generated ORM see
3
- # ingest_register_extra_routes. All routes stay auth-gated by the daemon
4
- # middleware (no @public_route), matching the pre-migration manifest.
1
+ # Behavior (the former main.nbt actions) lives in hand-written Jai over the
2
+ # generated ORM, which also registers the HTTP routes. All routes stay
3
+ # auth-gated by the daemon middleware (no @public_route), matching the
4
+ # pre-migration manifest.
5
5
 
6
6
  entity Endpoint {
7
7
  id: ulid
@@ -2,8 +2,8 @@
2
2
  # subscriptions. The portal owns browser permission prompts and service worker
3
3
  # registration; this cartridge owns the resulting state.
4
4
  #
5
- # Behavior (the former main.nbt actions) lives in native/runtime.jai as regular
6
- # Jai over the generated ORM see notifications_register_extra_routes.
5
+ # Behavior (the former main.nbt actions) lives in hand-written Jai over the
6
+ # generated ORM, which also registers the HTTP routes.
7
7
 
8
8
 
9
9
  entity Notification {
@@ -1,21 +1,20 @@
1
1
  # Phone cartridge — Twilio number management, SMS, click-to-call.
2
2
  #
3
- # Per-tenant model (see modules/twilio/module.jai for the rationale):
4
- # - One master Twilio account owned by the platform (SID + auth token in
5
- # env: TWILIO_MASTER_SID, TWILIO_MASTER_TOKEN).
3
+ # Per-tenant model:
4
+ # - One master Twilio account owned by the platform (master SID + auth token
5
+ # supplied via env).
6
6
  # - Each tenant gets a TwilioAccount row with its own subaccount SID and
7
7
  # auth token. All per-tenant API calls authenticate as the subaccount.
8
8
  # - Webhooks are configured to URLs under the tenant's public base
9
9
  # (TwilioAccount.publicUrlBase), which the gateway routes back to this
10
10
  # cart. Signature verification uses the subaccount's auth token.
11
11
 
12
- # Behavior (the former jai{} helper blocks + actions) lives in
13
- # native/runtime.jai over the generated ORM see phone_register_extra_routes,
14
- # which registers the HTTP routes.
12
+ # Behavior (the former actions) lives in hand-written Jai over the generated
13
+ # ORM, which also registers the HTTP routes.
15
14
 
16
- # Auth policy for the hand-written routes (formerly @public actions). The
17
- # handlers live in native/runtime.jai; these keep the daemon's public-route
18
- # manifest correct so the /api/* auth gate is bypassed for Twilio webhooks.
15
+ # Auth policy for the hand-written routes (formerly @public actions). These
16
+ # keep the daemon's public-route manifest correct so the /api/* auth gate is
17
+ # bypassed for Twilio webhooks.
19
18
  @public_route "/api/phone/webhook/sms"
20
19
  @public_route "/api/phone/webhook/status"
21
20
 
@@ -25,7 +24,7 @@ entity TwilioAccount {
25
24
  updatedAt: DateTime @updatedAt
26
25
  name: string # operator-facing label
27
26
  subAccountSid: string # AC... — populated by provision()
28
- authToken: string # subaccount auth token (TODO: encrypt at rest)
27
+ authToken: string # subaccount auth token (stored as-is)
29
28
  publicUrlBase: string # e.g. "https://acme.console.app"
30
29
  status: string # PENDING | ACTIVE | SUSPENDED | CLOSED
31
30
  retentionDays: s32 # 0 = keep message bodies forever; N = null body after N days
@@ -1,44 +1,92 @@
1
-
2
- # Question: How do we tie in cartridge metadata and workflow side effects into all this?
3
-
4
- enum WorkerStatus {
5
- WAITING
6
- RUNNING
7
- TERMINATED
8
- }
9
-
10
- entity Worker {
1
+ # An immutable, versioned workflow bundle (the JS artifact). Deploying new code
2
+ # registers a new version; in-flight executions keep replaying against the version
3
+ # they started on (the journal-header pin), so a code change can never corrupt a
4
+ # running instance. A version's source is retained while ≥1 execution pins it and
5
+ # dropped once the last drains (refcount drain-GC, in-memory). Durable so a pinned
6
+ # version survives restart.
7
+ entity WorkflowBundle {
11
8
  id: ulid
12
9
  createdAt: DateTime @default(now())
13
10
  updatedAt: DateTime @updatedAt
14
- source: blob
11
+ version: u32
12
+ source: string
15
13
  contentHash: string
16
- queue?: string
17
- # TODO: We need some sort of termination options like a restart policy
18
14
  }
19
15
 
20
- # Stub flesh out execution lifecycle fields later.
16
+ # A single durable workflow instance. The journal is the WorkflowExecutionEvent
17
+ # rows owned by this row (ordered by seq); status + result/error are the
18
+ # materialized terminal state. cursor = number of resolved host commands
19
+ # (== count of HOST_CALL_RESULT events) — the replay ordinal high-water mark.
21
20
  entity WorkflowExecution {
22
21
  id: ulid
23
22
  createdAt: DateTime @default(now())
24
23
  updatedAt: DateTime @updatedAt
25
- status: string
24
+ status: string # RUNNING (incl. async host call in flight) | SUSPENDED | COMPLETED | FAILED | CANCELLED
25
+ workflowName: string
26
+ args?: string # JSON arguments passed to runWorkflow
27
+ result?: string # JSON return value (COMPLETED)
28
+ error?: string # message + stack (FAILED)
29
+ cursor: u32
30
+ lane?: string # fairness lane; per-lane concurrency is capped so a burst of one lane can't starve others (default "default")
31
+ deployVersion: u32 # the bundle version this instance pins for its entire life (journal-header pin); replay always runs against this exact version
26
32
  events: WorkflowExecutionEvent[]
27
33
  }
28
34
 
29
- # Do we want this to be a string?
35
+ # A recurring trigger: fire `workflowName` whenever the cron expression matches.
36
+ # Idempotent per minute-bucket via a deterministic execution id (schedule + bucket),
37
+ # so a tick repeating within the same minute — or a restart mid-minute — never
38
+ # double-fires. Evaluated leader-only by the executor tick.
39
+ entity WorkflowSchedule {
40
+ id: ulid
41
+ createdAt: DateTime @default(now())
42
+ updatedAt: DateTime @updatedAt
43
+ workflowName: string
44
+ cron: string # 5-field: minute hour day-of-month month day-of-week
45
+ args?: string # JSON arguments passed to each fired execution
46
+ lane?: string # lane the fired executions run on
47
+ enabled: bool
48
+ lastFiredBucket: u32 # highest minute-bucket already fired; the durable idempotency watermark (no double-fire across ticks/restart)
49
+ }
50
+
51
+ # A durable record that a named event fired. Emissions ride the WAL (replicated
52
+ # like any row); the leader-only executor reconcile drains the unprocessed ones,
53
+ # looks up the event's triggers in the storage event registry, and fires each
54
+ # triggered workflow exactly once cluster-wide (only the leader drains; the fired
55
+ # id is derived from emission+index so a crash mid-drain re-fires idempotently).
56
+ entity WorkflowEvent {
57
+ id: ulid
58
+ createdAt: DateTime @default(now())
59
+ updatedAt: DateTime @updatedAt
60
+ event: string # "<slug>:<event>" — the event-registry key
61
+ payload?: string # JSON passed as args to each fired workflow
62
+ processed: bool # set true once the leader has fired this emission's triggers
63
+ }
64
+
30
65
  enum WorkflowExecutionEventKind {
31
- WORKFLOW_EXECUTION_STARTED
32
- WORKFLOW_STEP_SCHEDULED
33
- WORKFLOW_STEP_STARTED
34
- WORKFLOW_STEP_COMPLETED
35
- WORKFLOW_EXECUTION_COMPLETED
66
+ HOST_CALL_INTENT # recorded BEFORE a side effect is performed (durability)
67
+ HOST_CALL_RESULT # the resolved value of a host command (replay source)
68
+ SUSPENDED # parked awaiting an external command (e.g. a signal)
69
+ RESUMED # an external command arrived and re-drove the instance
70
+ COMPLETED # workflow function returned
71
+ FAILED # workflow function threw
72
+ CANCELLED # terminated by an explicit cancel request (cooperative)
73
+ RETRY # an async host call failed transiently and was retried (counter; payload = attempt)
74
+ CONTINUED # ended via continue-as-new; payload = the child execution id carrying the work forward
36
75
  }
37
76
 
77
+ # One journal entry. For HOST_CALL_* events seq is the command ordinal and
78
+ # (capability, target, op) is the descriptor; payload carries the args (INTENT)
79
+ # or the result JSON (RESULT). Lifecycle events (SUSPENDED/RESUMED/COMPLETED/
80
+ # FAILED/CANCELLED/RETRY) carry payload only.
38
81
  entity WorkflowExecutionEvent {
39
82
  id: ulid
40
83
  createdAt: DateTime @default(now())
41
84
  updatedAt: DateTime @updatedAt
42
85
  execution: WorkflowExecution @relation(onDelete: Cascade) # events are owned by their execution
86
+ seq: u32
43
87
  kind: WorkflowExecutionEventKind
88
+ capability?: string
89
+ target?: string
90
+ op?: string
91
+ payload?: string
44
92
  }
Binary file
Binary file
@@ -1,85 +0,0 @@
1
- # GHL Adapter
2
-
3
- Webhook ingest → CRM persistence as composable tasks. The adapter classifies
4
- inbound GHL Workflow webhook payloads and upserts the corresponding Contact
5
- or Deal directly. Appointment persistence lives in the calendar cart's
6
- parallel adapter (`cartridges/core/calendar/adapters/gohighlevel/`) because
7
- Calendar/Appointment are calendar-cart entities.
8
-
9
- No transformation, normalization, geocoding, AI scoring, or write-back to
10
- GHL. Those are downstream client-cart concerns. Read-side tasks (backfill,
11
- sync recovery) come back when there's a real consumer.
12
-
13
- ## Tasks (queue names = `crm.<task>`)
14
-
15
- | Task | Inputs | Outputs |
16
- |---|---|---|
17
- | `crm.ghl_classify_event_type` | `body_json` | `event_type`, `tenant_org`, `tenant_project` |
18
- | `crm.ghl_persist_contact` | `body_json` | `contact_id`, `created`, `ok`, `error` |
19
- | `crm.ghl_persist_opportunity` | `body_json` | `deal_id`, `created`, `ok`, `error` |
20
-
21
- Plus, in the calendar cart:
22
-
23
- | Task | Inputs | Outputs |
24
- |---|---|---|
25
- | `calendar.ghl_persist_appointment` | `body_json` | `appointment_id`, `calendar_id`, `ok`, `error` |
26
-
27
- `event_type` outputs from classify: `ContactCreate`, `ContactChange`,
28
- `OpportunityCreate`, `OpportunityChange`, `AppointmentCreate`,
29
- `AppointmentChange`, `Unknown`. The 7th GHL workflow "Pipeline Stage Changed
30
- Sync" aliases onto `OpportunityChange` because the payload shape is
31
- identical.
32
-
33
- ## Webhook entry
34
-
35
- GHL POSTs to ingest:
36
-
37
- ```
38
- POST /api/ingest/endpoint/receive?e=mylocalpro-ghl
39
- ```
40
-
41
- The `Endpoint` row for slug `mylocalpro-ghl` carries `systemId` pointing at
42
- the per-client System graph. After Payload persistence, ingest fires
43
- `queue_exec_start("system.<systemId>", { payloadId, endpointSlug, endpointId, body_json })`.
44
-
45
- The first task in the per-client graph is `crm.ghl_classify_event_type`,
46
- followed by a `branch` on `event_type` to one of the persist tasks
47
- (`ghl_persist_contact`, `ghl_persist_opportunity`,
48
- `calendar.ghl_persist_appointment`).
49
-
50
- ## Files
51
-
52
- ```
53
- main.nbt # composes the persist tasks
54
- native/persist.jai # ghl_apply_contact_payload, ghl_apply_opportunity_payload
55
- tasks/classify_event_type.nbt
56
- tasks/persist_contact.nbt
57
- tasks/persist_opportunity.nbt
58
- tests/fixtures/webhooks/ # 4 real-shape fixtures (contact_*/opportunity_*)
59
- tests/README.md # fixture provenance + classifier expectations
60
- ```
61
-
62
- Native imports are declared at the cart root (`crm/cartridge.nbt`) because
63
- the codegen's native-file copy step assumes paths are cart-root-relative.
64
-
65
- The calendar cart mirrors this layout for appointment persistence:
66
- `cartridges/core/calendar/adapters/gohighlevel/`.
67
-
68
- ## External-PK = local-PK
69
-
70
- GHL ids are written into entity `id` directly: a Contact for
71
- `contact_id="ctc_xyz"` lives at `Contact.id="ctc_xyz"`, an Opportunity for
72
- `id="opp_001"` lives at `Deal.id="opp_001"`, an Appointment for
73
- `calendar.appointmentId="apt_xyz"` lives at `Appointment.id="apt_xyz"`. No
74
- `externalId` indirection — the next webhook update keys the same row.
75
-
76
- ## Adding a persist branch
77
-
78
- 1. Add the helper to `native/persist.jai` (or a calendar/agent/etc. cart's
79
- `native/persist.jai` if the target entity lives there).
80
- 2. Create `tasks/persist_<name>.nbt` with `input body_json: string` and the
81
- handler delegating to the helper.
82
- 3. Import the task in `main.nbt`.
83
- 4. Update `tasks/classify_event_type.nbt` if a new `event_type` is needed.
84
- 5. `jai first.jai` — task appears in the cart's `contract.json` and at
85
- `/_console/task-catalog`.