@t4h.framework/core 0.3.0 → 0.5.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.
- package/.ai/skills/framework-core/SKILL.md +403 -0
- package/.ai/skills/framework-workflow-creation/SKILL.md +170 -0
- package/.ai/skills/framework-workflow-testing/SKILL.md +172 -0
- package/README.md +78 -5
- package/dist/core.d.ts +1 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +1 -0
- package/dist/core.js.map +1 -1
- package/dist/models/Logger.d.ts +13 -0
- package/dist/models/Logger.d.ts.map +1 -0
- package/dist/models/Logger.js +42 -0
- package/dist/models/Logger.js.map +1 -0
- package/dist/testing/WorkflowTesting.d.ts +6 -0
- package/dist/testing/WorkflowTesting.d.ts.map +1 -0
- package/dist/testing/WorkflowTesting.js +7 -0
- package/dist/testing/WorkflowTesting.js.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/run-workflow.d.ts +31 -0
- package/dist/testing/run-workflow.d.ts.map +1 -0
- package/dist/testing/run-workflow.js +102 -0
- package/dist/testing/run-workflow.js.map +1 -0
- package/dist/tools/Internals.d.ts +2 -0
- package/dist/tools/Internals.d.ts.map +1 -1
- package/dist/tools/Internals.js +2 -1
- package/dist/tools/Internals.js.map +1 -1
- package/dist/tools/runWorkflowToCompletion.d.ts +15 -0
- package/dist/tools/runWorkflowToCompletion.d.ts.map +1 -0
- package/dist/tools/runWorkflowToCompletion.js +61 -0
- package/dist/tools/runWorkflowToCompletion.js.map +1 -0
- package/package.json +11 -4
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framework-core
|
|
3
|
+
description: >-
|
|
4
|
+
Author and test T4H Framework workflows, SyncActivity/AsyncActivity classes,
|
|
5
|
+
Claim dependencies, App manifests, env access, and in-memory workflow runs.
|
|
6
|
+
Use when importing @t4h.framework/core, building automation apps, orchestrating
|
|
7
|
+
with History.reconciler, wiring Claim providers, testing with
|
|
8
|
+
@t4h.framework/core/testing (WorkflowTesting, mockAsyncActivity), nested
|
|
9
|
+
startWorkflow, EnvDesign, Authorization manifests, or Internals.runWorkflow /
|
|
10
|
+
Internals.runActivity.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# @t4h.framework/core
|
|
14
|
+
|
|
15
|
+
Core primitives for **Workflows** (orchestration), **Activities** (work units), and **Claims** (runtime dependencies). The runtime supplies `env`, `claims`, and `history`; this package defines the author API and in-memory test harness.
|
|
16
|
+
|
|
17
|
+
| Import | Purpose |
|
|
18
|
+
|--------|---------|
|
|
19
|
+
| `@t4h.framework/core` | Workflows, activities, claims, app, env, internals |
|
|
20
|
+
| `@t4h.framework/core/testing` | `WorkflowTesting` — tests/local only |
|
|
21
|
+
|
|
22
|
+
Node `>=22`. Schemas use TypeBox (`typebox`). See [README.md](../../../README.md) for diagrams.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Concepts
|
|
27
|
+
|
|
28
|
+
- **Workflow** — orchestration; composes activities via `History.reconciler` (or helpers like `startWorkflow`).
|
|
29
|
+
- **Activity** — `SyncActivity` (`toInput` → `run` → `toOutput`) or `AsyncActivity` (`toInput` → `bootstrap` → `advance` → `toOutput`, optional `onTimeout`).
|
|
30
|
+
- **Claim** — abstract class; activity maps keys → constructors; runtime provides `{ provide, value }`.
|
|
31
|
+
- **History** — replay: existing history entries skip execution; missing entries run activity and record result (`PushActivity`).
|
|
32
|
+
|
|
33
|
+
Async helpers on `AsyncActivity`: `this.pending()`, `this.done()`, `this.rejected()`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Workflows
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import Type from 'typebox'
|
|
41
|
+
import { Workflow, env } from '@t4h.framework/core'
|
|
42
|
+
|
|
43
|
+
const bare = new Workflow(async () => ({ ok: true }))
|
|
44
|
+
|
|
45
|
+
const withMeta = new Workflow(
|
|
46
|
+
{ id: 'cashback/v1', description: 'Process cashback' },
|
|
47
|
+
async input => ({ value: input.amount * 0.05 }),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const schema = Type.Object({ userId: Type.String(), amount: Type.Number() })
|
|
51
|
+
const validated = new Workflow({ id: 'cashback/v1', schema }, async input => ({
|
|
52
|
+
value: input.amount * 0.05,
|
|
53
|
+
}))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `new Workflow(fn)` or `new Workflow(options, fn)` — bad overload throws `Invalid constructor arguments`.
|
|
57
|
+
- `WorkflowInput<T>` — from schema; `undefined` without schema.
|
|
58
|
+
- `workflow.validate` — Ajv when `schema` set; else `null`.
|
|
59
|
+
- Optional `authorization` on workflow/app → `toManifest()`.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Invoking activities
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { History } from '@t4h.framework/core'
|
|
67
|
+
|
|
68
|
+
const syncResult = await History.reconciler(MySyncActivity, arg1)
|
|
69
|
+
|
|
70
|
+
const handle = await History.reconciler(WaitAsyncActivity, 'order-1')
|
|
71
|
+
const out = await handle.wait()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- No history: `toInput` then `PushActivity` → runtime runs `run`/`bootstrap`.
|
|
75
|
+
- Replay: uses stored output; async returns `AsyncActivityAwaitable`; `wait()` throws `PushWaitAsyncActivity` if still `pending`.
|
|
76
|
+
- Must run inside `History.run` (workflow runtime / `WorkflowTesting` provide this).
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## SyncActivity
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { SyncActivity } from '@t4h.framework/core'
|
|
84
|
+
|
|
85
|
+
class CalculateCashbackActivity extends SyncActivity<
|
|
86
|
+
[transaction: { amount: number }], // TArgs
|
|
87
|
+
{ amount: number }, // TInput (Serializable)
|
|
88
|
+
number, // TSerializableOutput
|
|
89
|
+
number // TOutput
|
|
90
|
+
> {
|
|
91
|
+
public async toInput(transaction: { amount: number }) {
|
|
92
|
+
return { amount: transaction.amount }
|
|
93
|
+
}
|
|
94
|
+
public run(input: { amount: number }) {
|
|
95
|
+
return Math.floor(input.amount * 0.05)
|
|
96
|
+
}
|
|
97
|
+
public toOutput(output: number) {
|
|
98
|
+
return output
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`toOutput(output, ...args)` receives original designer args.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## AsyncActivity
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import Type from 'typebox'
|
|
111
|
+
import { AsyncActivity, AsyncActivityResultPending } from '@t4h.framework/core'
|
|
112
|
+
|
|
113
|
+
const ApprovalSchema = Type.Object({ approved: Type.Boolean() })
|
|
114
|
+
|
|
115
|
+
class WaitForApprovalActivity extends AsyncActivity<
|
|
116
|
+
[orderId: string],
|
|
117
|
+
{ orderId: string },
|
|
118
|
+
{ approved: boolean },
|
|
119
|
+
{ approved: boolean },
|
|
120
|
+
{ requestId: string; orderId: string },
|
|
121
|
+
typeof ApprovalSchema
|
|
122
|
+
> {
|
|
123
|
+
public async toInput(orderId: string) {
|
|
124
|
+
return { orderId }
|
|
125
|
+
}
|
|
126
|
+
public async bootstrap(input: { orderId: string }) {
|
|
127
|
+
return this.pending({
|
|
128
|
+
payload: { requestId: 'req-123', orderId: input.orderId },
|
|
129
|
+
schema: ApprovalSchema,
|
|
130
|
+
timeoutAt: new Date(Date.now() + 3_600_000),
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
public async advance(
|
|
134
|
+
payload: Type.Static<typeof ApprovalSchema>,
|
|
135
|
+
bootstrap: AsyncActivityResultPending<{ requestId: string; orderId: string }>,
|
|
136
|
+
) {
|
|
137
|
+
return payload.approved
|
|
138
|
+
? this.done({ approved: true })
|
|
139
|
+
: this.rejected({ approved: false, requestId: bootstrap.payload?.requestId })
|
|
140
|
+
}
|
|
141
|
+
public toOutput(output: { approved: boolean }) {
|
|
142
|
+
return output
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Results: `AsyncActivityResultPending` | `AsyncActivityResultFulfilled` | `AsyncActivityResultRejected`. Default `onTimeout` → `rejected({ reason: 'timeout' })`.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Claims
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { SyncActivity, Claim } from '@t4h.framework/core'
|
|
155
|
+
|
|
156
|
+
abstract class UserRepositoryClaim {
|
|
157
|
+
abstract findById(id: string): Promise<unknown>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class FetchUserActivity extends SyncActivity<[id: string], { id: string }, unknown, unknown> {
|
|
161
|
+
private readonly claims = new Claim({ repo: UserRepositoryClaim })
|
|
162
|
+
|
|
163
|
+
public async toInput(id: string) {
|
|
164
|
+
return { id }
|
|
165
|
+
}
|
|
166
|
+
public async run(input: { id: string }) {
|
|
167
|
+
return await this.claims.repo.findById(input.id)
|
|
168
|
+
}
|
|
169
|
+
public toOutput(output: unknown) {
|
|
170
|
+
return output
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Provider: `{ provide: UserRepositoryClaim, value: impl }`. Built-ins: `GetWorkflowUniqueIdentifierClaim`, `GetActivityUniqueIdentifierClaim`, `WorkflowClaim` (for `startWorkflow`).
|
|
176
|
+
|
|
177
|
+
Exceptions: `ClaimKeyNotFoundException`, `ClaimConstructorNotFoundException`, `ClaimStorageNotActiveException`, `ClaimProviderNotFoundException`. Access `claims.*` only inside `Claim.run`.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Nested workflows
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { startWorkflow, WorkflowClaim } from '@t4h.framework/core'
|
|
185
|
+
|
|
186
|
+
const ref = await startWorkflow(childWorkflow, { ...input, parallel?: boolean })
|
|
187
|
+
// WorkflowRef: status 'fulfilled' | 'rejected', output Serializable
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`StartWorkflowActivity`: `parallel: true` → immediate `done`; else `pending` until `advance`. Requires `WorkflowClaim` provider.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## App
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { App, Workflow } from '@t4h.framework/core'
|
|
198
|
+
|
|
199
|
+
const app = new App({
|
|
200
|
+
id: 'my-automation',
|
|
201
|
+
description: 'Cashback automation',
|
|
202
|
+
workflows: [myWorkflow],
|
|
203
|
+
})
|
|
204
|
+
app.addWorkflow(anotherWorkflow) // throws on duplicate workflow.id
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`toManifest()` includes workflows + registered `EnvDesign` entries.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Environment
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { env, EnvDesign } from '@t4h.framework/core'
|
|
215
|
+
|
|
216
|
+
// Under Env.run — string values
|
|
217
|
+
console.log(env.API_URL)
|
|
218
|
+
|
|
219
|
+
// Typed parsing (registers for app manifest)
|
|
220
|
+
const config = EnvDesign({
|
|
221
|
+
API_URL: { type: String },
|
|
222
|
+
RETRIES: { type: Number, defaultValue: '3' },
|
|
223
|
+
DEBUG: { type: Boolean, isOptional: true },
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Serializable & Authorization
|
|
230
|
+
|
|
231
|
+
**Serializable:** primitives, `null`, `undefined`, `Binary`, `SerializableClass`, arrays/objects thereof. `Binary`: `contentType`, `contentLength`, `stream()`.
|
|
232
|
+
|
|
233
|
+
**Authorization:** subclass with `toManifest(): AuthorizationManifest` (`pathToModule`, optional `context`); attach to `Workflow` or `App`.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Internals (runtime / unit tests)
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { Internals } from '@t4h.framework/core'
|
|
241
|
+
|
|
242
|
+
Internals.runWorkflow(workflow, {
|
|
243
|
+
input,
|
|
244
|
+
env: { API_URL: 'https://api.example.com' },
|
|
245
|
+
claims: [{ provide: UserRepositoryClaim, value: impl }],
|
|
246
|
+
history: [],
|
|
247
|
+
logs: [],
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
Internals.runActivity(FetchUserActivity, { env, claims }, a => a.run({ id: '123' }))
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Stack: `Env.run` → `Claim.run` → `History.run` → `Logger.run` (workflows). **Test `advance` here** — not via `WorkflowTesting`.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Testing (`@t4h.framework/core/testing`)
|
|
258
|
+
|
|
259
|
+
Test-only entrypoint — **not** part of the production runtime contract. Import from the subpath export:
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { WorkflowTesting } from '@t4h.framework/core/testing'
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`package.json` maps `"./testing"` → `dist/testing/index.js`. The public API is **`WorkflowTesting.run`** only.
|
|
266
|
+
|
|
267
|
+
### `WorkflowTesting.run`
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
const output = await WorkflowTesting.run(workflow, input?, options?)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
| Option | Type | Default | Purpose |
|
|
274
|
+
|--------|------|---------|---------|
|
|
275
|
+
| `claims` | `readonly ClaimProvider[]` | `[]` | Concrete claim implementations for the run |
|
|
276
|
+
| `env` | `Env.Entries` | `{}` | String env vars (`Env.run`) |
|
|
277
|
+
| `mockAsyncActivity` | function \| outcome array | — | Required when any async activity returns `pending` |
|
|
278
|
+
| `logs` | `Log[]` | internal | Capture `console.log` / `console.error` once per run |
|
|
279
|
+
| `maxTicks` | `number` | `10_000` | Max activity-resolution loops before error |
|
|
280
|
+
|
|
281
|
+
Typed input follows `WorkflowInput<T>` when the workflow has a `schema`; omit `input` for workflows without one.
|
|
282
|
+
|
|
283
|
+
### How the driver works
|
|
284
|
+
|
|
285
|
+
1. Calls `Internals.runWorkflow` with an empty `history`.
|
|
286
|
+
2. On **`PushActivity`**, runs the pushed activity in-process:
|
|
287
|
+
- **Sync:** `Internals.runActivity` → `run(input)` → append `{ type: 'sync', output }` to history.
|
|
288
|
+
- **Async:** real `bootstrap`; if already `done`/`rejected`, record and continue; if **`pending`**, apply `mockAsyncActivity` (throws if missing).
|
|
289
|
+
3. Re-runs the workflow with accumulated history until it returns normally or `maxTicks` is exceeded.
|
|
290
|
+
|
|
291
|
+
This drives sync activities end-to-end and async activities through bootstrap + mocked resolution — **not** through real `advance`.
|
|
292
|
+
|
|
293
|
+
### Wiring claims in tests
|
|
294
|
+
|
|
295
|
+
Pass explicit `{ provide, value }` providers — same shape as `Internals.runWorkflow`:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { WorkflowTesting } from '@t4h.framework/core/testing'
|
|
299
|
+
import { History, type Log } from '@t4h.framework/core'
|
|
300
|
+
|
|
301
|
+
const logs: Log[] = []
|
|
302
|
+
const output = await WorkflowTesting.run(workflow, input, {
|
|
303
|
+
env: { API_URL: 'https://example.com' },
|
|
304
|
+
claims: [{ provide: UserRepositoryClaim, value: mockRepo }],
|
|
305
|
+
mockAsyncActivity: () => ({ status: 'fulfilled', output: true }),
|
|
306
|
+
logs,
|
|
307
|
+
})
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
For a single activity, prefer `Claim.run([...], fn)` or `Internals.runActivity`. Use `WorkflowTesting.run` for full workflow integration tests.
|
|
311
|
+
|
|
312
|
+
### `mockAsyncActivity`
|
|
313
|
+
|
|
314
|
+
Required when `bootstrap` returns `pending`:
|
|
315
|
+
|
|
316
|
+
| `status` | Effect |
|
|
317
|
+
|----------|--------|
|
|
318
|
+
| `fulfilled` | `{ output }` recorded as async fulfilled |
|
|
319
|
+
| `rejected` | `{ reason }` recorded as async rejected |
|
|
320
|
+
| `timeout` | runs the activity's real `onTimeout(pending)` (throws if undefined) |
|
|
321
|
+
|
|
322
|
+
**Array** — FIFO, one entry per pending async:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
mockAsyncActivity: [
|
|
326
|
+
{ status: 'fulfilled', output: true },
|
|
327
|
+
{ status: 'rejected', reason: false },
|
|
328
|
+
]
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Function** — `(activity, { input, pending, index }) => outcome`:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
mockAsyncActivity: (activity, { index }) => {
|
|
335
|
+
if (activity instanceof WaitForApprovalActivity)
|
|
336
|
+
return { status: 'fulfilled', output: true }
|
|
337
|
+
if (index === 1) return { status: 'timeout' }
|
|
338
|
+
return { status: 'rejected', reason: 'denied' }
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Bootstrap that already returns `done`/`rejected` needs no mock. Missing mock on pending throws: `pending and no resolveAsyncActivity`. Exhausted array throws: `No more async outcomes`.
|
|
343
|
+
|
|
344
|
+
### Examples (from `WorkflowTesting.spec.ts`)
|
|
345
|
+
|
|
346
|
+
**Sync workflow:**
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
const workflow = new Workflow({ schema }, async input => {
|
|
350
|
+
const n = await History.reconciler(AddOneActivity, input.value)
|
|
351
|
+
return n * 10
|
|
352
|
+
})
|
|
353
|
+
await expect(WorkflowTesting.run(workflow, { value: 4 })).resolves.toBe(50)
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Capture logs:**
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const logs: Log[] = []
|
|
360
|
+
await WorkflowTesting.run(workflow, undefined, { logs })
|
|
361
|
+
expect(logs.map(entry => entry.args[0])).toEqual(['before activity', 'after activity'])
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Async fulfilled / rejected / timeout:**
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
await WorkflowTesting.run(workflow, undefined, {
|
|
368
|
+
mockAsyncActivity: () => ({ status: 'fulfilled', output: true }),
|
|
369
|
+
})
|
|
370
|
+
await WorkflowTesting.run(workflow, undefined, {
|
|
371
|
+
mockAsyncActivity: () => ({ status: 'rejected', reason: false }),
|
|
372
|
+
})
|
|
373
|
+
await WorkflowTesting.run(workflow, undefined, {
|
|
374
|
+
mockAsyncActivity: () => ({ status: 'timeout' }),
|
|
375
|
+
})
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Vitest setup** — add `@t4h.framework/core` and `@t4h.framework/core/testing` as devDependencies; run with Vitest (`vitest run`). See `packages/core/src/testing/__tests__/WorkflowTesting.spec.ts`.
|
|
379
|
+
|
|
380
|
+
### Limits (use `Internals` instead)
|
|
381
|
+
|
|
382
|
+
- No real **`advance`** — test `advance` via `Internals.runActivity` on the async class.
|
|
383
|
+
- No **`PushWaitAsyncActivity`** / resuming pending `handle.wait()` across ticks.
|
|
384
|
+
- Do not import `@t4h.framework/core/testing` in production app code.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Anti-patterns
|
|
389
|
+
|
|
390
|
+
- Claims or `env` outside `Claim.run` / `Env.run`.
|
|
391
|
+
- `History.reconciler` outside `History.run`.
|
|
392
|
+
- Pending async tests without `mockAsyncActivity`.
|
|
393
|
+
- `advance` coverage only via `WorkflowTesting` (use `Internals.runActivity`).
|
|
394
|
+
- Non-`Serializable` activity I/O.
|
|
395
|
+
- Duplicate `workflow.id` in `App`.
|
|
396
|
+
- `@t4h.framework/core/testing` in production.
|
|
397
|
+
- APIs not exported from `core.ts` / `testing/index.ts`.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Exports (main)
|
|
402
|
+
|
|
403
|
+
`Workflow`, `App`, `SyncActivity`, `AsyncActivity`, `Claim`, `History`, `env`, `EnvDesign`, `Authorization`, `WorkflowRef`, `Binary`, `Serializable`, async result types, `startWorkflow`, `WorkflowClaim`, `Internals`, `isActivity`, `Logger`, `Activity` types, exceptions. **Testing:** `WorkflowTesting` only.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framework-workflow-creation
|
|
3
|
+
description: >-
|
|
4
|
+
Design and implement T4H Framework workflows with @t4h.framework/core using
|
|
5
|
+
the project's standard structure for workflow files, app registration, and
|
|
6
|
+
environment handling. Use this whenever the user asks to create a new
|
|
7
|
+
workflow, wire it into an App, define orchestration with History.reconciler,
|
|
8
|
+
or refactor workflow code to remove hardcoded URLs/secrets/client values and
|
|
9
|
+
move configuration to env constants.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Create workflows with `@t4h.framework/core`
|
|
13
|
+
|
|
14
|
+
Use this skill to build production-ready workflow modules that are easy to register, test, and maintain.
|
|
15
|
+
|
|
16
|
+
## Primary goal
|
|
17
|
+
|
|
18
|
+
Create workflows that:
|
|
19
|
+
|
|
20
|
+
- expose a `Workflow` constant with explicit metadata (`id`, optional `description`, optional `schema`);
|
|
21
|
+
- orchestrate work through framework primitives (`History.reconciler`, `startWorkflow`) instead of ad-hoc infrastructure code;
|
|
22
|
+
- register cleanly in `App` with `export default new App({...})`;
|
|
23
|
+
- never hardcode URLs, secrets, tokens, client IDs, or tenants in workflow files.
|
|
24
|
+
|
|
25
|
+
## Expected project shape
|
|
26
|
+
|
|
27
|
+
### Workflow module
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { Workflow } from '@t4h.framework/core'
|
|
31
|
+
|
|
32
|
+
export const myWorkflow = new Workflow({ id: 'my-workflow' }, async input => {
|
|
33
|
+
// orchestration only
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### App declaration
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { App } from '@t4h.framework/core'
|
|
41
|
+
import { myWorkflow } from './workflows/my-workflow.js'
|
|
42
|
+
|
|
43
|
+
export default new App({
|
|
44
|
+
id: 'my-app',
|
|
45
|
+
description: 'My app',
|
|
46
|
+
workflows: [myWorkflow],
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Environment constants module
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { env } from '@t4h.framework/core'
|
|
54
|
+
|
|
55
|
+
export const SERVICE_URL = env.SERVICE_URL!
|
|
56
|
+
if (!SERVICE_URL) throw new TypeError('SERVICE_URL is not defined')
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Keep this in a dedicated constants file and import values into workflows/activities.
|
|
60
|
+
|
|
61
|
+
## Core APIs to prefer while authoring workflows
|
|
62
|
+
|
|
63
|
+
- `Workflow` for orchestration entrypoints.
|
|
64
|
+
- `History.reconciler(ActivityClass, ...args)` to execute activities deterministically.
|
|
65
|
+
- `startWorkflow(childWorkflow, { ...input, parallel? })` for nested workflow orchestration.
|
|
66
|
+
- `App` to register workflow modules for runtime discovery.
|
|
67
|
+
- `env` / `EnvDesign` for configuration contracts (not inline literal secrets).
|
|
68
|
+
|
|
69
|
+
## Authoring rules
|
|
70
|
+
|
|
71
|
+
1. Keep workflows orchestration-focused. Put low-level operations in activities and claims.
|
|
72
|
+
2. Define workflow metadata with stable `id`.
|
|
73
|
+
3. Use schema when input validation is needed.
|
|
74
|
+
4. Import configuration from environment constants.
|
|
75
|
+
5. Keep side effects explicit and easy to assert in tests.
|
|
76
|
+
|
|
77
|
+
## Hard constraints
|
|
78
|
+
|
|
79
|
+
- Do not hardcode URLs, client IDs, client secrets, API keys, tenants, or tokens.
|
|
80
|
+
- Do not initialize third-party clients with inline credentials inside workflow files.
|
|
81
|
+
- Do not read `process.env` directly in workflow modules when the project uses centralized constants via `env`.
|
|
82
|
+
|
|
83
|
+
If a required env key is missing, fail fast with a clear `TypeError` in the constants module.
|
|
84
|
+
|
|
85
|
+
## Workflow implementation recipe
|
|
86
|
+
|
|
87
|
+
### Step 1: define/extend env constants
|
|
88
|
+
|
|
89
|
+
- Add required keys in a constants module.
|
|
90
|
+
- Export each key from `env.<KEY>!`.
|
|
91
|
+
- Validate each key immediately with an explicit `TypeError`.
|
|
92
|
+
|
|
93
|
+
### Step 2: create workflow file
|
|
94
|
+
|
|
95
|
+
- Import `Workflow` and required orchestration helpers.
|
|
96
|
+
- Import only typed/safe constants (never raw secrets inline).
|
|
97
|
+
- Implement the function body as orchestration steps.
|
|
98
|
+
|
|
99
|
+
### Step 3: wire in App
|
|
100
|
+
|
|
101
|
+
- Import the workflow module in `main.tsx`.
|
|
102
|
+
- Add it to `workflows: [...]` in `new App({...})`.
|
|
103
|
+
- Keep `App` declaration as default export for runtime loading.
|
|
104
|
+
|
|
105
|
+
### Step 4: make it test-friendly
|
|
106
|
+
|
|
107
|
+
- Keep outputs deterministic where possible.
|
|
108
|
+
- Avoid hidden global state.
|
|
109
|
+
- Structure side effects so they can be mocked by claim/activity tests.
|
|
110
|
+
|
|
111
|
+
## Recommended patterns
|
|
112
|
+
|
|
113
|
+
### A) Workflow with validated input schema
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import Type from 'typebox'
|
|
117
|
+
import { Workflow } from '@t4h.framework/core'
|
|
118
|
+
|
|
119
|
+
const schema = Type.Object({
|
|
120
|
+
customerId: Type.String(),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
export const customerSync = new Workflow(
|
|
124
|
+
{ id: 'customer-sync', schema },
|
|
125
|
+
async input => {
|
|
126
|
+
return { accepted: true, customerId: input.customerId }
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### B) Workflow orchestrating activities
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { History, Workflow } from '@t4h.framework/core'
|
|
135
|
+
|
|
136
|
+
export const processOrder = new Workflow({ id: 'process-order' }, async input => {
|
|
137
|
+
const prepared = await History.reconciler(PrepareOrderActivity, input)
|
|
138
|
+
const charged = await History.reconciler(ChargeOrderActivity, prepared)
|
|
139
|
+
return { charged }
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### C) Nested workflow orchestration
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { Workflow, startWorkflow } from '@t4h.framework/core'
|
|
147
|
+
import { childWorkflow } from './child-workflow.js'
|
|
148
|
+
|
|
149
|
+
export const parentWorkflow = new Workflow({ id: 'parent-workflow' }, async input => {
|
|
150
|
+
const ref = await startWorkflow(childWorkflow, input)
|
|
151
|
+
return ref.output
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Anti-patterns to remove on sight
|
|
156
|
+
|
|
157
|
+
- Inline `"https://..."` service endpoints in workflow body.
|
|
158
|
+
- Inline `"secret"` / `"clientId"` string literals in workflow code.
|
|
159
|
+
- Creating SDK/API clients with literal credentials in the workflow function.
|
|
160
|
+
- Large workflow functions doing infrastructure plumbing instead of orchestration.
|
|
161
|
+
- Registering workflows in scattered places instead of centralized `App` declaration.
|
|
162
|
+
|
|
163
|
+
## Output expectations when executing this skill
|
|
164
|
+
|
|
165
|
+
When asked to create a workflow, produce:
|
|
166
|
+
|
|
167
|
+
1. A new workflow file exporting a named `Workflow` constant.
|
|
168
|
+
2. Any required env constants in the central constants module.
|
|
169
|
+
3. `App` registration update with the workflow included.
|
|
170
|
+
4. No hardcoded client, secret, token, tenant, or URL values in workflow code.
|