@frontmcp/skills 0.0.1
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/LICENSE +201 -0
- package/README.md +135 -0
- package/catalog/TEMPLATE.md +49 -0
- package/catalog/adapters/create-adapter/SKILL.md +127 -0
- package/catalog/adapters/official-adapters/SKILL.md +136 -0
- package/catalog/auth/configure-auth/SKILL.md +250 -0
- package/catalog/auth/configure-auth/references/auth-modes.md +77 -0
- package/catalog/auth/configure-session/SKILL.md +201 -0
- package/catalog/config/configure-elicitation/SKILL.md +136 -0
- package/catalog/config/configure-http/SKILL.md +167 -0
- package/catalog/config/configure-throttle/SKILL.md +189 -0
- package/catalog/config/configure-throttle/references/guard-config.md +68 -0
- package/catalog/config/configure-transport/SKILL.md +151 -0
- package/catalog/config/configure-transport/references/protocol-presets.md +57 -0
- package/catalog/deployment/build-for-browser/SKILL.md +95 -0
- package/catalog/deployment/build-for-cli/SKILL.md +100 -0
- package/catalog/deployment/build-for-sdk/SKILL.md +218 -0
- package/catalog/deployment/deploy-to-cloudflare/SKILL.md +192 -0
- package/catalog/deployment/deploy-to-lambda/SKILL.md +304 -0
- package/catalog/deployment/deploy-to-node/SKILL.md +229 -0
- package/catalog/deployment/deploy-to-node/references/Dockerfile.example +45 -0
- package/catalog/deployment/deploy-to-vercel/SKILL.md +196 -0
- package/catalog/deployment/deploy-to-vercel/references/vercel.json.example +60 -0
- package/catalog/development/create-agent/SKILL.md +563 -0
- package/catalog/development/create-agent/references/llm-config.md +46 -0
- package/catalog/development/create-job/SKILL.md +566 -0
- package/catalog/development/create-prompt/SKILL.md +400 -0
- package/catalog/development/create-provider/SKILL.md +233 -0
- package/catalog/development/create-resource/SKILL.md +437 -0
- package/catalog/development/create-skill/SKILL.md +526 -0
- package/catalog/development/create-skill-with-tools/SKILL.md +579 -0
- package/catalog/development/create-tool/SKILL.md +418 -0
- package/catalog/development/create-tool/references/output-schema-types.md +56 -0
- package/catalog/development/create-tool/references/tool-annotations.md +34 -0
- package/catalog/development/create-workflow/SKILL.md +709 -0
- package/catalog/development/decorators-guide/SKILL.md +598 -0
- package/catalog/plugins/create-plugin/SKILL.md +336 -0
- package/catalog/plugins/create-plugin-hooks/SKILL.md +282 -0
- package/catalog/plugins/official-plugins/SKILL.md +667 -0
- package/catalog/setup/frontmcp-skills-usage/SKILL.md +200 -0
- package/catalog/setup/multi-app-composition/SKILL.md +358 -0
- package/catalog/setup/nx-workflow/SKILL.md +357 -0
- package/catalog/setup/project-structure-nx/SKILL.md +186 -0
- package/catalog/setup/project-structure-standalone/SKILL.md +153 -0
- package/catalog/setup/setup-project/SKILL.md +493 -0
- package/catalog/setup/setup-redis/SKILL.md +385 -0
- package/catalog/setup/setup-sqlite/SKILL.md +359 -0
- package/catalog/skills-manifest.json +414 -0
- package/catalog/testing/setup-testing/SKILL.md +539 -0
- package/catalog/testing/setup-testing/references/test-auth.md +88 -0
- package/catalog/testing/setup-testing/references/test-browser-build.md +57 -0
- package/catalog/testing/setup-testing/references/test-cli-binary.md +48 -0
- package/catalog/testing/setup-testing/references/test-direct-client.md +62 -0
- package/catalog/testing/setup-testing/references/test-e2e-handler.md +51 -0
- package/catalog/testing/setup-testing/references/test-tool-unit.md +41 -0
- package/package.json +34 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +16 -0
- package/src/index.js.map +1 -0
- package/src/loader.d.ts +46 -0
- package/src/loader.js +75 -0
- package/src/loader.js.map +1 -0
- package/src/manifest.d.ts +81 -0
- package/src/manifest.js +26 -0
- package/src/manifest.js.map +1 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-workflow
|
|
3
|
+
description: Create multi-step workflows that connect jobs into managed execution pipelines with dependencies and conditions. Use when orchestrating sequential or parallel job execution.
|
|
4
|
+
tags: [workflow, pipeline, orchestration, steps, jobs]
|
|
5
|
+
priority: 6
|
|
6
|
+
visibility: both
|
|
7
|
+
license: Apache-2.0
|
|
8
|
+
metadata:
|
|
9
|
+
docs: https://docs.agentfront.dev/frontmcp/servers/workflows
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Creating Workflows
|
|
13
|
+
|
|
14
|
+
Workflows connect multiple jobs into managed execution pipelines with step dependencies, conditions, and triggers. A workflow defines a directed acyclic graph (DAG) of steps where each step runs a named job, and the framework handles ordering, parallelism, error propagation, and trigger management.
|
|
15
|
+
|
|
16
|
+
## When to Use @Workflow
|
|
17
|
+
|
|
18
|
+
Use `@Workflow` when you need to orchestrate multiple jobs in a defined order with dependencies between them. Examples include:
|
|
19
|
+
|
|
20
|
+
- CI/CD pipelines (build, test, deploy)
|
|
21
|
+
- Data processing pipelines (extract, transform, load, verify)
|
|
22
|
+
- Approval workflows (submit, review, approve, execute)
|
|
23
|
+
- Multi-stage provisioning (create resources, configure, validate, notify)
|
|
24
|
+
|
|
25
|
+
If you only need a single background task, use a `@Job` instead. If you need real-time sequential tool calls guided by an AI, use a `@Skill`.
|
|
26
|
+
|
|
27
|
+
## Class-Based Pattern
|
|
28
|
+
|
|
29
|
+
Create a class decorated with `@Workflow`. The decorator requires `name` and `steps` (at least one step).
|
|
30
|
+
|
|
31
|
+
### WorkflowMetadata Fields
|
|
32
|
+
|
|
33
|
+
| Field | Type | Required | Default | Description |
|
|
34
|
+
| ---------------- | ---------------------------------- | ----------- | ----------------- | ----------------------------------------------------- |
|
|
35
|
+
| `name` | `string` | Yes | -- | Unique workflow name |
|
|
36
|
+
| `steps` | `WorkflowStep[]` | Yes (min 1) | -- | Array of step definitions |
|
|
37
|
+
| `description` | `string` | No | -- | Human-readable description |
|
|
38
|
+
| `trigger` | `'manual' \| 'webhook' \| 'event'` | No | `'manual'` | How the workflow is initiated |
|
|
39
|
+
| `webhook` | `WebhookConfig` | No | -- | Webhook configuration (when trigger is `'webhook'`) |
|
|
40
|
+
| `timeout` | `number` | No | `600000` (10 min) | Maximum total workflow execution time in milliseconds |
|
|
41
|
+
| `maxConcurrency` | `number` | No | `5` | Maximum number of steps running in parallel |
|
|
42
|
+
| `permissions` | `WorkflowPermissions` | No | -- | Access control configuration |
|
|
43
|
+
|
|
44
|
+
### WorkflowStep Fields
|
|
45
|
+
|
|
46
|
+
| Field | Type | Required | Description |
|
|
47
|
+
| ----------------- | ------------------------------------------ | -------- | --------------------------------------------------------------------------- |
|
|
48
|
+
| `id` | `string` | Yes | Unique step identifier within the workflow |
|
|
49
|
+
| `jobName` | `string` | Yes | Name of the registered job to run |
|
|
50
|
+
| `input` | `object \| (steps: StepResults) => object` | No | Static input object or function that receives previous step results |
|
|
51
|
+
| `dependsOn` | `string[]` | No | Array of step IDs that must complete before this step runs |
|
|
52
|
+
| `condition` | `(steps: StepResults) => boolean` | No | Predicate that determines if the step should run |
|
|
53
|
+
| `continueOnError` | `boolean` | No | If `true`, workflow continues even if this step fails |
|
|
54
|
+
| `timeout` | `number` | No | Per-step timeout in milliseconds (overrides workflow timeout for this step) |
|
|
55
|
+
| `retry` | `RetryPolicy` | No | Per-step retry policy (overrides the job's retry policy for this step) |
|
|
56
|
+
|
|
57
|
+
### Basic Example
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { Workflow } from '@frontmcp/sdk';
|
|
61
|
+
|
|
62
|
+
@Workflow({
|
|
63
|
+
name: 'deploy-pipeline',
|
|
64
|
+
description: 'Build, test, and deploy a service',
|
|
65
|
+
steps: [
|
|
66
|
+
{
|
|
67
|
+
id: 'build',
|
|
68
|
+
jobName: 'build-project',
|
|
69
|
+
input: { target: 'production', optimize: true },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'test',
|
|
73
|
+
jobName: 'run-tests',
|
|
74
|
+
input: { suite: 'all', coverage: true },
|
|
75
|
+
dependsOn: ['build'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'deploy',
|
|
79
|
+
jobName: 'deploy-to-env',
|
|
80
|
+
input: (steps) => ({
|
|
81
|
+
artifact: steps.get('build').outputs.artifactUrl,
|
|
82
|
+
environment: 'production',
|
|
83
|
+
}),
|
|
84
|
+
dependsOn: ['test'],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
})
|
|
88
|
+
class DeployPipeline {}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Step Dependencies and DAG Execution
|
|
92
|
+
|
|
93
|
+
Steps form a directed acyclic graph (DAG) based on their `dependsOn` declarations. The framework:
|
|
94
|
+
|
|
95
|
+
1. Identifies steps with no dependencies and runs them in parallel (up to `maxConcurrency`)
|
|
96
|
+
2. As each step completes, checks which dependent steps have all their dependencies satisfied
|
|
97
|
+
3. Runs newly unblocked steps in parallel
|
|
98
|
+
4. Continues until all steps complete or a step fails (unless `continueOnError` is set)
|
|
99
|
+
|
|
100
|
+
### Parallel Steps
|
|
101
|
+
|
|
102
|
+
Steps without mutual dependencies run concurrently:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
@Workflow({
|
|
106
|
+
name: 'data-validation-pipeline',
|
|
107
|
+
description: 'Validate data from multiple sources in parallel, then merge',
|
|
108
|
+
maxConcurrency: 3,
|
|
109
|
+
steps: [
|
|
110
|
+
// These three steps have no dependencies -- they run in parallel
|
|
111
|
+
{
|
|
112
|
+
id: 'validate-users',
|
|
113
|
+
jobName: 'validate-dataset',
|
|
114
|
+
input: { dataset: 'users', rules: ['no-nulls', 'email-format'] },
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'validate-orders',
|
|
118
|
+
jobName: 'validate-dataset',
|
|
119
|
+
input: { dataset: 'orders', rules: ['no-nulls', 'positive-amounts'] },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'validate-products',
|
|
123
|
+
jobName: 'validate-dataset',
|
|
124
|
+
input: { dataset: 'products', rules: ['no-nulls', 'unique-sku'] },
|
|
125
|
+
},
|
|
126
|
+
// This step depends on all three -- runs after all complete
|
|
127
|
+
{
|
|
128
|
+
id: 'merge-results',
|
|
129
|
+
jobName: 'merge-validations',
|
|
130
|
+
dependsOn: ['validate-users', 'validate-orders', 'validate-products'],
|
|
131
|
+
input: (steps) => ({
|
|
132
|
+
userReport: steps.get('validate-users').outputs,
|
|
133
|
+
orderReport: steps.get('validate-orders').outputs,
|
|
134
|
+
productReport: steps.get('validate-products').outputs,
|
|
135
|
+
}),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
})
|
|
139
|
+
class DataValidationPipeline {}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Diamond Dependencies
|
|
143
|
+
|
|
144
|
+
Steps can share dependencies, forming diamond patterns:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
@Workflow({
|
|
148
|
+
name: 'build-and-publish',
|
|
149
|
+
description: 'Build artifacts and publish to multiple registries',
|
|
150
|
+
steps: [
|
|
151
|
+
{ id: 'compile', jobName: 'compile-source', input: { target: 'es2022' } },
|
|
152
|
+
{
|
|
153
|
+
id: 'publish-npm',
|
|
154
|
+
jobName: 'publish-to-registry',
|
|
155
|
+
dependsOn: ['compile'],
|
|
156
|
+
input: (steps) => ({ artifact: steps.get('compile').outputs.bundlePath, registry: 'npm' }),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'publish-docker',
|
|
160
|
+
jobName: 'publish-to-registry',
|
|
161
|
+
dependsOn: ['compile'],
|
|
162
|
+
input: (steps) => ({ artifact: steps.get('compile').outputs.bundlePath, registry: 'docker' }),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'notify',
|
|
166
|
+
jobName: 'send-notification',
|
|
167
|
+
dependsOn: ['publish-npm', 'publish-docker'],
|
|
168
|
+
input: (steps) => ({
|
|
169
|
+
message: `Published to npm (${steps.get('publish-npm').outputs.version}) and Docker (${steps.get('publish-docker').outputs.tag})`,
|
|
170
|
+
}),
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
})
|
|
174
|
+
class BuildAndPublish {}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Dynamic Input from Previous Steps
|
|
178
|
+
|
|
179
|
+
Use a function for `input` to pass data from completed steps. The function receives a `StepResults` map where each entry contains the step's state and outputs.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
{
|
|
183
|
+
id: 'transform',
|
|
184
|
+
jobName: 'transform-data',
|
|
185
|
+
dependsOn: ['extract'],
|
|
186
|
+
input: (steps) => ({
|
|
187
|
+
data: steps.get('extract').outputs.records,
|
|
188
|
+
schema: steps.get('extract').outputs.schema,
|
|
189
|
+
rowCount: steps.get('extract').outputs.count,
|
|
190
|
+
}),
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The `steps.get(stepId)` method returns a step result object:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
interface StepResult {
|
|
198
|
+
state: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
199
|
+
outputs: Record<string, unknown>; // job output from the completed step
|
|
200
|
+
error?: string; // error message if the step failed
|
|
201
|
+
startedAt?: string;
|
|
202
|
+
completedAt?: string;
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Conditional Steps
|
|
207
|
+
|
|
208
|
+
Use `condition` to conditionally run a step based on the results of previous steps. The condition receives the same `StepResults` map.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
@Workflow({
|
|
212
|
+
name: 'conditional-deploy',
|
|
213
|
+
description: 'Deploy only if tests pass and coverage meets threshold',
|
|
214
|
+
steps: [
|
|
215
|
+
{
|
|
216
|
+
id: 'test',
|
|
217
|
+
jobName: 'run-tests',
|
|
218
|
+
input: { suite: 'all', coverage: true },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'deploy',
|
|
222
|
+
jobName: 'deploy-to-env',
|
|
223
|
+
dependsOn: ['test'],
|
|
224
|
+
condition: (steps) => {
|
|
225
|
+
const testResult = steps.get('test');
|
|
226
|
+
return testResult.state === 'completed' && testResult.outputs.coverage >= 95;
|
|
227
|
+
},
|
|
228
|
+
input: (steps) => ({
|
|
229
|
+
artifact: steps.get('test').outputs.buildPath,
|
|
230
|
+
environment: 'staging',
|
|
231
|
+
}),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'notify-failure',
|
|
235
|
+
jobName: 'send-notification',
|
|
236
|
+
dependsOn: ['test'],
|
|
237
|
+
condition: (steps) => steps.get('test').state === 'failed',
|
|
238
|
+
input: { channel: '#alerts', message: 'Test suite failed -- deployment blocked' },
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
})
|
|
242
|
+
class ConditionalDeploy {}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
When a `condition` returns `false`, the step is marked as `skipped`. Downstream steps that depend on a skipped step check their own conditions with the skipped step's state.
|
|
246
|
+
|
|
247
|
+
## Error Handling with continueOnError
|
|
248
|
+
|
|
249
|
+
By default, a failed step stops the entire workflow. Set `continueOnError: true` on a step to allow the workflow to proceed even if that step fails.
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
@Workflow({
|
|
253
|
+
name: 'resilient-pipeline',
|
|
254
|
+
description: 'Pipeline that continues past non-critical failures',
|
|
255
|
+
steps: [
|
|
256
|
+
{
|
|
257
|
+
id: 'extract',
|
|
258
|
+
jobName: 'extract-data',
|
|
259
|
+
input: { source: 'primary-db' },
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'enrich',
|
|
263
|
+
jobName: 'enrich-data',
|
|
264
|
+
dependsOn: ['extract'],
|
|
265
|
+
continueOnError: true, // enrichment is optional
|
|
266
|
+
input: (steps) => ({ data: steps.get('extract').outputs.records }),
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: 'load',
|
|
270
|
+
jobName: 'load-data',
|
|
271
|
+
dependsOn: ['extract', 'enrich'],
|
|
272
|
+
input: (steps) => {
|
|
273
|
+
const enrichResult = steps.get('enrich');
|
|
274
|
+
// Use enriched data if available, fall back to raw
|
|
275
|
+
const data =
|
|
276
|
+
enrichResult.state === 'completed'
|
|
277
|
+
? enrichResult.outputs.enrichedRecords
|
|
278
|
+
: steps.get('extract').outputs.records;
|
|
279
|
+
return { data, destination: 'warehouse' };
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
})
|
|
284
|
+
class ResilientPipeline {}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Workflow Triggers
|
|
288
|
+
|
|
289
|
+
### Manual (Default)
|
|
290
|
+
|
|
291
|
+
The workflow is started by an explicit API call or MCP request:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
@Workflow({
|
|
295
|
+
name: 'manual-deploy',
|
|
296
|
+
description: 'Manually triggered deployment',
|
|
297
|
+
trigger: 'manual',
|
|
298
|
+
steps: [
|
|
299
|
+
/* ... */
|
|
300
|
+
],
|
|
301
|
+
})
|
|
302
|
+
class ManualDeploy {}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Webhook
|
|
306
|
+
|
|
307
|
+
The workflow is triggered by an incoming HTTP request. Configure the webhook path, secret, and allowed HTTP methods.
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
@Workflow({
|
|
311
|
+
name: 'github-deploy',
|
|
312
|
+
description: 'Deploy on GitHub push events',
|
|
313
|
+
trigger: 'webhook',
|
|
314
|
+
webhook: {
|
|
315
|
+
path: '/webhooks/github-deploy',
|
|
316
|
+
secret: process.env.WEBHOOK_SECRET,
|
|
317
|
+
methods: ['POST'],
|
|
318
|
+
},
|
|
319
|
+
steps: [
|
|
320
|
+
{
|
|
321
|
+
id: 'build',
|
|
322
|
+
jobName: 'build-project',
|
|
323
|
+
input: { branch: 'main' },
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: 'deploy',
|
|
327
|
+
jobName: 'deploy-to-env',
|
|
328
|
+
dependsOn: ['build'],
|
|
329
|
+
input: (steps) => ({
|
|
330
|
+
artifact: steps.get('build').outputs.artifactUrl,
|
|
331
|
+
environment: 'production',
|
|
332
|
+
}),
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
})
|
|
336
|
+
class GithubDeploy {}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### WebhookConfig Fields
|
|
340
|
+
|
|
341
|
+
| Field | Type | Default | Description |
|
|
342
|
+
| --------- | ---------- | --------------------------------- | ---------------------------------------------- |
|
|
343
|
+
| `path` | `string` | Auto-generated from workflow name | HTTP path for the webhook endpoint |
|
|
344
|
+
| `secret` | `string` | -- | Shared secret for webhook signature validation |
|
|
345
|
+
| `methods` | `string[]` | `['POST']` | Allowed HTTP methods |
|
|
346
|
+
|
|
347
|
+
### Event
|
|
348
|
+
|
|
349
|
+
The workflow is triggered by an internal event emitted by the application:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
@Workflow({
|
|
353
|
+
name: 'on-user-signup',
|
|
354
|
+
description: 'Workflow triggered when a new user signs up',
|
|
355
|
+
trigger: 'event',
|
|
356
|
+
steps: [
|
|
357
|
+
{
|
|
358
|
+
id: 'create-profile',
|
|
359
|
+
jobName: 'create-user-profile',
|
|
360
|
+
input: { template: 'default' },
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: 'send-welcome',
|
|
364
|
+
jobName: 'send-email',
|
|
365
|
+
dependsOn: ['create-profile'],
|
|
366
|
+
input: (steps) => ({
|
|
367
|
+
to: steps.get('create-profile').outputs.email,
|
|
368
|
+
template: 'welcome',
|
|
369
|
+
}),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
id: 'setup-defaults',
|
|
373
|
+
jobName: 'setup-user-defaults',
|
|
374
|
+
dependsOn: ['create-profile'],
|
|
375
|
+
input: (steps) => ({
|
|
376
|
+
userId: steps.get('create-profile').outputs.userId,
|
|
377
|
+
}),
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
})
|
|
381
|
+
class OnUserSignup {}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Function Builder
|
|
385
|
+
|
|
386
|
+
For workflows that do not need a class, use the `workflow()` function builder:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { workflow } from '@frontmcp/sdk';
|
|
390
|
+
|
|
391
|
+
const QuickDeploy = workflow({
|
|
392
|
+
name: 'quick-deploy',
|
|
393
|
+
description: 'Simplified deployment workflow',
|
|
394
|
+
steps: [
|
|
395
|
+
{
|
|
396
|
+
id: 'build',
|
|
397
|
+
jobName: 'build-project',
|
|
398
|
+
input: { target: 'production' },
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: 'deploy',
|
|
402
|
+
jobName: 'deploy-to-env',
|
|
403
|
+
dependsOn: ['build'],
|
|
404
|
+
input: (steps) => ({
|
|
405
|
+
artifact: steps.get('build').outputs.artifactUrl,
|
|
406
|
+
environment: 'staging',
|
|
407
|
+
}),
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Register it the same way as a class workflow: `workflows: [QuickDeploy]`.
|
|
414
|
+
|
|
415
|
+
## Registration
|
|
416
|
+
|
|
417
|
+
Add workflow classes (or function-style workflows) to the `workflows` array in `@App`. Workflows require jobs to be enabled since each step runs a named job.
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import { FrontMcp, App } from '@frontmcp/sdk';
|
|
421
|
+
|
|
422
|
+
@App({
|
|
423
|
+
name: 'pipeline-app',
|
|
424
|
+
jobs: [BuildProjectJob, RunTestsJob, DeployToEnvJob, SendNotificationJob],
|
|
425
|
+
workflows: [DeployPipeline, DataValidationPipeline, QuickDeploy],
|
|
426
|
+
})
|
|
427
|
+
class PipelineApp {}
|
|
428
|
+
|
|
429
|
+
@FrontMcp({
|
|
430
|
+
info: { name: 'pipeline-server', version: '1.0.0' },
|
|
431
|
+
apps: [PipelineApp],
|
|
432
|
+
jobs: {
|
|
433
|
+
enabled: true,
|
|
434
|
+
store: {
|
|
435
|
+
redis: {
|
|
436
|
+
provider: 'redis',
|
|
437
|
+
host: 'localhost',
|
|
438
|
+
port: 6379,
|
|
439
|
+
keyPrefix: 'mcp:jobs:',
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
})
|
|
444
|
+
class PipelineServer {}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Nx Generator
|
|
448
|
+
|
|
449
|
+
Scaffold a new workflow using the Nx generator:
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
nx generate @frontmcp/nx:workflow
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
This creates the workflow file, spec file, and updates barrel exports.
|
|
456
|
+
|
|
457
|
+
## Complete Example: CI/CD Pipeline
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
import { Workflow, Job, JobContext, FrontMcp, App, workflow } from '@frontmcp/sdk';
|
|
461
|
+
import { z } from 'zod';
|
|
462
|
+
|
|
463
|
+
// --- Jobs ---
|
|
464
|
+
|
|
465
|
+
@Job({
|
|
466
|
+
name: 'checkout-code',
|
|
467
|
+
description: 'Checkout code from repository',
|
|
468
|
+
inputSchema: {
|
|
469
|
+
repo: z.string().describe('Repository URL'),
|
|
470
|
+
branch: z.string().default('main'),
|
|
471
|
+
},
|
|
472
|
+
outputSchema: {
|
|
473
|
+
workDir: z.string(),
|
|
474
|
+
commitSha: z.string(),
|
|
475
|
+
},
|
|
476
|
+
})
|
|
477
|
+
class CheckoutCodeJob extends JobContext {
|
|
478
|
+
async execute(input: { repo: string; branch: string }) {
|
|
479
|
+
this.log(`Checking out ${input.repo}@${input.branch}`);
|
|
480
|
+
return { workDir: '/tmp/build/workspace', commitSha: 'abc123' };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@Job({
|
|
485
|
+
name: 'run-linter',
|
|
486
|
+
description: 'Run linter on codebase',
|
|
487
|
+
inputSchema: {
|
|
488
|
+
workDir: z.string(),
|
|
489
|
+
},
|
|
490
|
+
outputSchema: {
|
|
491
|
+
passed: z.boolean(),
|
|
492
|
+
issues: z.number().int(),
|
|
493
|
+
},
|
|
494
|
+
})
|
|
495
|
+
class RunLinterJob extends JobContext {
|
|
496
|
+
async execute(input: { workDir: string }) {
|
|
497
|
+
this.log(`Linting ${input.workDir}`);
|
|
498
|
+
return { passed: true, issues: 0 };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
@Job({
|
|
503
|
+
name: 'run-unit-tests',
|
|
504
|
+
description: 'Run unit test suite',
|
|
505
|
+
inputSchema: {
|
|
506
|
+
workDir: z.string(),
|
|
507
|
+
coverage: z.boolean().default(true),
|
|
508
|
+
},
|
|
509
|
+
outputSchema: {
|
|
510
|
+
passed: z.boolean(),
|
|
511
|
+
testCount: z.number().int(),
|
|
512
|
+
coverage: z.number(),
|
|
513
|
+
},
|
|
514
|
+
retry: { maxAttempts: 2, backoffMs: 3000, backoffMultiplier: 1, maxBackoffMs: 3000 },
|
|
515
|
+
})
|
|
516
|
+
class RunUnitTestsJob extends JobContext {
|
|
517
|
+
async execute(input: { workDir: string; coverage: boolean }) {
|
|
518
|
+
this.log(`Running unit tests in ${input.workDir}`);
|
|
519
|
+
this.progress(50, 100, 'Tests running');
|
|
520
|
+
return { passed: true, testCount: 342, coverage: 96.4 };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@Job({
|
|
525
|
+
name: 'build-artifact',
|
|
526
|
+
description: 'Build production artifact',
|
|
527
|
+
inputSchema: {
|
|
528
|
+
workDir: z.string(),
|
|
529
|
+
commitSha: z.string(),
|
|
530
|
+
},
|
|
531
|
+
outputSchema: {
|
|
532
|
+
artifactUrl: z.string().url(),
|
|
533
|
+
size: z.number().int(),
|
|
534
|
+
},
|
|
535
|
+
timeout: 180000,
|
|
536
|
+
})
|
|
537
|
+
class BuildArtifactJob extends JobContext {
|
|
538
|
+
async execute(input: { workDir: string; commitSha: string }) {
|
|
539
|
+
this.log(`Building artifact from ${input.commitSha}`);
|
|
540
|
+
this.progress(0, 100, 'Compiling');
|
|
541
|
+
this.progress(100, 100, 'Build complete');
|
|
542
|
+
return {
|
|
543
|
+
artifactUrl: `https://artifacts.example.com/builds/${input.commitSha}.tar.gz`,
|
|
544
|
+
size: 52428800,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
@Job({
|
|
550
|
+
name: 'deploy-artifact',
|
|
551
|
+
description: 'Deploy artifact to target environment',
|
|
552
|
+
inputSchema: {
|
|
553
|
+
artifactUrl: z.string().url(),
|
|
554
|
+
environment: z.string(),
|
|
555
|
+
},
|
|
556
|
+
outputSchema: {
|
|
557
|
+
deploymentId: z.string(),
|
|
558
|
+
url: z.string().url(),
|
|
559
|
+
},
|
|
560
|
+
retry: { maxAttempts: 3, backoffMs: 5000, backoffMultiplier: 2, maxBackoffMs: 30000 },
|
|
561
|
+
permissions: {
|
|
562
|
+
actions: ['execute'],
|
|
563
|
+
roles: ['admin', 'deployer'],
|
|
564
|
+
scopes: ['deploy:write'],
|
|
565
|
+
},
|
|
566
|
+
})
|
|
567
|
+
class DeployArtifactJob extends JobContext {
|
|
568
|
+
async execute(input: { artifactUrl: string; environment: string }) {
|
|
569
|
+
this.log(`Deploying ${input.artifactUrl} to ${input.environment}`);
|
|
570
|
+
return {
|
|
571
|
+
deploymentId: 'deploy-001',
|
|
572
|
+
url: `https://${input.environment}.example.com`,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
@Job({
|
|
578
|
+
name: 'notify-team',
|
|
579
|
+
description: 'Send notification to the team',
|
|
580
|
+
inputSchema: {
|
|
581
|
+
channel: z.string(),
|
|
582
|
+
message: z.string(),
|
|
583
|
+
},
|
|
584
|
+
outputSchema: {
|
|
585
|
+
sent: z.boolean(),
|
|
586
|
+
},
|
|
587
|
+
})
|
|
588
|
+
class NotifyTeamJob extends JobContext {
|
|
589
|
+
async execute(input: { channel: string; message: string }) {
|
|
590
|
+
this.log(`Notifying ${input.channel}: ${input.message}`);
|
|
591
|
+
return { sent: true };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// --- Workflow ---
|
|
596
|
+
|
|
597
|
+
@Workflow({
|
|
598
|
+
name: 'ci-cd-pipeline',
|
|
599
|
+
description: 'Full CI/CD pipeline: checkout, lint, test, build, deploy, notify',
|
|
600
|
+
trigger: 'webhook',
|
|
601
|
+
webhook: {
|
|
602
|
+
path: '/webhooks/ci-cd',
|
|
603
|
+
secret: process.env.CI_WEBHOOK_SECRET,
|
|
604
|
+
methods: ['POST'],
|
|
605
|
+
},
|
|
606
|
+
timeout: 900000, // 15 minutes
|
|
607
|
+
maxConcurrency: 3,
|
|
608
|
+
permissions: {
|
|
609
|
+
actions: ['create', 'read', 'execute', 'list'],
|
|
610
|
+
roles: ['admin', 'ci-bot'],
|
|
611
|
+
},
|
|
612
|
+
steps: [
|
|
613
|
+
{
|
|
614
|
+
id: 'checkout',
|
|
615
|
+
jobName: 'checkout-code',
|
|
616
|
+
input: { repo: 'https://github.com/org/repo.git', branch: 'main' },
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
id: 'lint',
|
|
620
|
+
jobName: 'run-linter',
|
|
621
|
+
dependsOn: ['checkout'],
|
|
622
|
+
input: (steps) => ({
|
|
623
|
+
workDir: steps.get('checkout').outputs.workDir,
|
|
624
|
+
}),
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
id: 'test',
|
|
628
|
+
jobName: 'run-unit-tests',
|
|
629
|
+
dependsOn: ['checkout'],
|
|
630
|
+
input: (steps) => ({
|
|
631
|
+
workDir: steps.get('checkout').outputs.workDir,
|
|
632
|
+
coverage: true,
|
|
633
|
+
}),
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
id: 'build',
|
|
637
|
+
jobName: 'build-artifact',
|
|
638
|
+
dependsOn: ['lint', 'test'],
|
|
639
|
+
condition: (steps) =>
|
|
640
|
+
steps.get('lint').state === 'completed' &&
|
|
641
|
+
steps.get('lint').outputs.passed === true &&
|
|
642
|
+
steps.get('test').state === 'completed' &&
|
|
643
|
+
steps.get('test').outputs.passed === true &&
|
|
644
|
+
steps.get('test').outputs.coverage >= 95,
|
|
645
|
+
input: (steps) => ({
|
|
646
|
+
workDir: steps.get('checkout').outputs.workDir,
|
|
647
|
+
commitSha: steps.get('checkout').outputs.commitSha,
|
|
648
|
+
}),
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
id: 'deploy',
|
|
652
|
+
jobName: 'deploy-artifact',
|
|
653
|
+
dependsOn: ['build'],
|
|
654
|
+
condition: (steps) => steps.get('build').state === 'completed',
|
|
655
|
+
input: (steps) => ({
|
|
656
|
+
artifactUrl: steps.get('build').outputs.artifactUrl,
|
|
657
|
+
environment: 'staging',
|
|
658
|
+
}),
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
id: 'notify-success',
|
|
662
|
+
jobName: 'notify-team',
|
|
663
|
+
dependsOn: ['deploy'],
|
|
664
|
+
condition: (steps) => steps.get('deploy').state === 'completed',
|
|
665
|
+
input: (steps) => ({
|
|
666
|
+
channel: '#deployments',
|
|
667
|
+
message: `Deployed ${steps.get('deploy').outputs.deploymentId} to ${steps.get('deploy').outputs.url}`,
|
|
668
|
+
}),
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
id: 'notify-failure',
|
|
672
|
+
jobName: 'notify-team',
|
|
673
|
+
dependsOn: ['lint', 'test'],
|
|
674
|
+
condition: (steps) => steps.get('lint').state === 'failed' || steps.get('test').state === 'failed',
|
|
675
|
+
input: {
|
|
676
|
+
channel: '#alerts',
|
|
677
|
+
message: 'CI pipeline failed -- check lint and test results',
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
})
|
|
682
|
+
class CiCdPipeline {}
|
|
683
|
+
|
|
684
|
+
// --- Registration ---
|
|
685
|
+
|
|
686
|
+
@App({
|
|
687
|
+
name: 'ci-app',
|
|
688
|
+
jobs: [CheckoutCodeJob, RunLinterJob, RunUnitTestsJob, BuildArtifactJob, DeployArtifactJob, NotifyTeamJob],
|
|
689
|
+
workflows: [CiCdPipeline],
|
|
690
|
+
})
|
|
691
|
+
class CiApp {}
|
|
692
|
+
|
|
693
|
+
@FrontMcp({
|
|
694
|
+
info: { name: 'ci-server', version: '1.0.0' },
|
|
695
|
+
apps: [CiApp],
|
|
696
|
+
jobs: {
|
|
697
|
+
enabled: true,
|
|
698
|
+
store: {
|
|
699
|
+
redis: {
|
|
700
|
+
provider: 'redis',
|
|
701
|
+
host: 'localhost',
|
|
702
|
+
port: 6379,
|
|
703
|
+
keyPrefix: 'mcp:ci:',
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
})
|
|
708
|
+
class CiServer {}
|
|
709
|
+
```
|