@tailor-platform/erp-kit 0.1.0 → 0.1.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +64 -44
  4. package/package.json +4 -2
  5. package/skills/1-module-docs/SKILL.md +4 -0
  6. package/{rules/module-development → skills/1-module-docs/references}/structure.md +2 -7
  7. package/skills/2-module-feature-breakdown/SKILL.md +6 -0
  8. package/{rules/module-development → skills/2-module-feature-breakdown/references}/commands.md +0 -6
  9. package/{rules/module-development → skills/2-module-feature-breakdown/references}/models.md +0 -5
  10. package/skills/2-module-feature-breakdown/references/structure.md +22 -0
  11. package/skills/3-module-doc-review/SKILL.md +6 -0
  12. package/skills/3-module-doc-review/references/commands.md +54 -0
  13. package/skills/3-module-doc-review/references/models.md +29 -0
  14. package/{rules/module-development → skills/3-module-doc-review/references}/testing.md +0 -6
  15. package/skills/4-module-tdd-implementation/SKILL.md +24 -6
  16. package/skills/4-module-tdd-implementation/references/commands.md +45 -0
  17. package/{rules/sdk-best-practices → skills/4-module-tdd-implementation/references}/db-relations.md +0 -5
  18. package/{rules/module-development → skills/4-module-tdd-implementation/references}/errors.md +0 -5
  19. package/{rules/module-development → skills/4-module-tdd-implementation/references}/exports.md +0 -5
  20. package/skills/4-module-tdd-implementation/references/models.md +30 -0
  21. package/skills/4-module-tdd-implementation/references/structure.md +22 -0
  22. package/skills/4-module-tdd-implementation/references/testing.md +37 -0
  23. package/skills/5-module-implementation-review/SKILL.md +8 -0
  24. package/skills/5-module-implementation-review/references/commands.md +45 -0
  25. package/skills/5-module-implementation-review/references/errors.md +7 -0
  26. package/skills/5-module-implementation-review/references/exports.md +8 -0
  27. package/skills/5-module-implementation-review/references/models.md +30 -0
  28. package/skills/5-module-implementation-review/references/testing.md +29 -0
  29. package/skills/app-compose-1-requirement-analysis/SKILL.md +4 -0
  30. package/{rules/app-compose → skills/app-compose-1-requirement-analysis/references}/structure.md +0 -5
  31. package/skills/app-compose-2-requirements-breakdown/SKILL.md +7 -0
  32. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-detailview.md +0 -6
  33. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-form.md +0 -6
  34. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-listview.md +0 -6
  35. package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
  36. package/skills/app-compose-3-doc-review/SKILL.md +4 -0
  37. package/skills/app-compose-3-doc-review/references/structure.md +27 -0
  38. package/skills/app-compose-4-design-mock/SKILL.md +8 -0
  39. package/{rules/app-compose/frontend → skills/app-compose-4-design-mock/references}/component.md +0 -5
  40. package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
  41. package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
  42. package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
  43. package/skills/app-compose-4-design-mock/references/structure.md +27 -0
  44. package/skills/app-compose-5-design-mock-review/SKILL.md +7 -0
  45. package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
  46. package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
  47. package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
  48. package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
  49. package/skills/app-compose-6-implementation-spec/SKILL.md +5 -0
  50. package/{rules/app-compose/backend → skills/app-compose-6-implementation-spec/references}/auth.md +0 -6
  51. package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
  52. package/src/cli.ts +2 -2
  53. package/src/commands/init.test.ts +30 -19
  54. package/src/commands/init.ts +76 -43
  55. package/rules/app-compose/frontend/auth.md +0 -55
  56. package/rules/app-compose/frontend/page.md +0 -86
  57. package/rules/module-development/cross-module-type-injection.md +0 -28
  58. package/rules/module-development/dependency-modules.md +0 -24
  59. package/rules/module-development/executors.md +0 -67
  60. package/rules/module-development/sync-vs-async-operations.md +0 -83
  61. package/rules/sdk-best-practices/sdk-docs.md +0 -14
@@ -4,7 +4,6 @@ import chalk from "chalk";
4
4
  import { PACKAGE_ROOT } from "../util.js";
5
5
 
6
6
  const SKILLS_SRC = path.join(PACKAGE_ROOT, "skills");
7
- const RULES_SRC = path.join(PACKAGE_ROOT, "rules");
8
7
 
9
8
  const FRAMEWORK_SKILLS = [
10
9
  "1-module-docs",
@@ -21,65 +20,99 @@ const FRAMEWORK_SKILLS = [
21
20
  "mock-scenario",
22
21
  ];
23
22
 
23
+ function copyDirectoryRecursive(
24
+ srcDir: string,
25
+ destDir: string,
26
+ force: boolean,
27
+ ): { copied: number; skipped: number } {
28
+ let copied = 0;
29
+ let skipped = 0;
30
+
31
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
32
+ const srcPath = path.join(srcDir, entry.name);
33
+ const destPath = path.join(destDir, entry.name);
34
+
35
+ if (entry.isDirectory()) {
36
+ const sub = copyDirectoryRecursive(srcPath, destPath, force);
37
+ copied += sub.copied;
38
+ skipped += sub.skipped;
39
+ } else {
40
+ if (!force && fs.existsSync(destPath)) {
41
+ skipped++;
42
+ continue;
43
+ }
44
+ fs.mkdirSync(destDir, { recursive: true });
45
+ fs.copyFileSync(srcPath, destPath);
46
+ copied++;
47
+ }
48
+ }
49
+
50
+ return { copied, skipped };
51
+ }
52
+
24
53
  export function runInit(cwd: string, force: boolean): number {
25
54
  console.log(chalk.bold("erp-kit init\n"));
26
55
 
56
+ // --- Skills ---
27
57
  const skillsDest = path.join(cwd, ".agents", "skills");
28
58
  let copiedCount = 0;
29
59
  let skippedCount = 0;
30
60
  for (const skill of FRAMEWORK_SKILLS) {
31
- const srcSkill = path.join(SKILLS_SRC, skill, "SKILL.md");
61
+ const srcSkillDir = path.join(SKILLS_SRC, skill);
62
+ if (!fs.existsSync(srcSkillDir)) continue;
63
+
32
64
  const destDir = path.join(skillsDest, skill);
33
- const destSkill = path.join(destDir, "SKILL.md");
65
+ const result = copyDirectoryRecursive(srcSkillDir, destDir, force);
66
+ copiedCount += result.copied;
34
67
 
35
- if (!fs.existsSync(srcSkill)) continue;
36
- if (!force && fs.existsSync(destSkill)) {
37
- console.log(chalk.yellow(` Skipped ${skill}/SKILL.md (already exists)`));
38
- skippedCount++;
39
- continue;
68
+ if (result.skipped > 0) {
69
+ console.log(chalk.yellow(` Skipped ${skill}/ (${result.skipped} existing files)`));
70
+ skippedCount += result.skipped;
40
71
  }
41
-
42
- fs.mkdirSync(destDir, { recursive: true });
43
- fs.copyFileSync(srcSkill, destSkill);
44
- copiedCount++;
45
72
  }
46
- console.log(chalk.green(` Copied ${copiedCount} framework skills to .agents/skills/`));
73
+ console.log(chalk.green(` Copied ${copiedCount} skill files to .agents/skills/`));
47
74
  if (skippedCount > 0) {
48
75
  console.log(
49
- chalk.yellow(` Skipped ${skippedCount} existing skills (use --force to overwrite)`),
76
+ chalk.yellow(` Skipped ${skippedCount} existing files (use --force to overwrite)`),
50
77
  );
51
78
  }
52
79
 
53
- const rulesDest = path.join(cwd, ".agents", "rules");
54
- let rulesCount = 0;
55
- let rulesSkipped = 0;
56
- if (fs.existsSync(RULES_SRC)) {
57
- const copyRulesRecursive = (srcDir: string, destDir: string) => {
58
- for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
59
- const srcPath = path.join(srcDir, entry.name);
60
- const destPath = path.join(destDir, entry.name);
61
- if (entry.isDirectory()) {
62
- copyRulesRecursive(srcPath, destPath);
63
- } else {
64
- if (!force && fs.existsSync(destPath)) {
65
- const rel = path.relative(rulesDest, destPath);
66
- console.log(chalk.yellow(` Skipped rule ${rel} (already exists)`));
67
- rulesSkipped++;
68
- continue;
69
- }
70
- fs.mkdirSync(destDir, { recursive: true });
71
- fs.copyFileSync(srcPath, destPath);
72
- rulesCount++;
73
- }
80
+ // --- Claude Code symlink ---
81
+ const claudeSkills = path.join(cwd, ".claude", "skills");
82
+ const relTarget = path.relative(path.dirname(claudeSkills), skillsDest);
83
+
84
+ // lstatSync doesn't follow symlinks, so dangling symlinks are detected
85
+ const claudeSkillsExists = (() => {
86
+ try {
87
+ fs.lstatSync(claudeSkills);
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ })();
93
+
94
+ if (claudeSkillsExists) {
95
+ const stat = fs.lstatSync(claudeSkills);
96
+ if (stat.isSymbolicLink()) {
97
+ const current = fs.readlinkSync(claudeSkills);
98
+ if (current === relTarget) {
99
+ console.log(chalk.green(" .claude/skills -> .agents/skills/ (already linked)"));
100
+ } else if (force) {
101
+ fs.unlinkSync(claudeSkills);
102
+ fs.symlinkSync(relTarget, claudeSkills);
103
+ console.log(chalk.green(" .claude/skills -> .agents/skills/ (relinked)"));
104
+ } else {
105
+ console.log(
106
+ chalk.yellow(` Skipped .claude/skills (symlink exists -> ${current}, use --force)`),
107
+ );
74
108
  }
75
- };
76
- copyRulesRecursive(RULES_SRC, rulesDest);
77
- }
78
- console.log(chalk.green(` Copied ${rulesCount} framework rules to .agents/rules/`));
79
- if (rulesSkipped > 0) {
80
- console.log(
81
- chalk.yellow(` Skipped ${rulesSkipped} existing rules (use --force to overwrite)`),
82
- );
109
+ } else {
110
+ console.log(chalk.yellow(" Skipped .claude/skills (directory exists, not a symlink)"));
111
+ }
112
+ } else {
113
+ fs.mkdirSync(path.dirname(claudeSkills), { recursive: true });
114
+ fs.symlinkSync(relTarget, claudeSkills);
115
+ console.log(chalk.green(" .claude/skills -> .agents/skills/ (linked)"));
83
116
  }
84
117
 
85
118
  console.log(chalk.bold.green("\nDone! Run `erp-kit check` to validate your docs."));
@@ -1,55 +0,0 @@
1
- ---
2
- paths:
3
- - "examples/**/frontend/src/App.tsx"
4
- - "examples/**/frontend/src/lib/auth-client.ts"
5
- - "examples/**/frontend/src/providers/graphql-provider.tsx"
6
- ---
7
-
8
- # Frontend Auth (AppShell AuthProvider)
9
-
10
- Use `@tailor-platform/app-shell` authentication as the default pattern for frontend apps.
11
-
12
- ## Required Environment Variables
13
-
14
- - `VITE_TAILOR_APP_URL`
15
- - `VITE_TAILOR_CLIENT_ID`
16
-
17
- Fail fast at startup if either value is missing.
18
-
19
- ## Auth Client Initialization
20
-
21
- Create a single shared auth client in `src/lib/auth-client.ts`:
22
-
23
- - Use `createAuthClient({ appUri, clientId })`
24
- - Export it as `authClient`
25
- - Do not create auth clients inside render functions
26
-
27
- ## App Root Composition
28
-
29
- Wrap the app with `AuthProvider` in `src/App.tsx`:
30
-
31
- - `AuthProvider` must receive the shared `authClient`
32
- - Use `guardComponent` for unauthenticated/loading states
33
- - `guardComponent` should use `useAuth()` and handle:
34
- - `!isReady`: loading UI
35
- - `!isAuthenticated`: login UI with `login()`
36
- - `error`: visible message
37
-
38
- Recommended order:
39
-
40
- 1. `AuthProvider`
41
- 2. `GraphQLProvider`
42
- 3. `AppShell`
43
-
44
- ## GraphQL Authentication
45
-
46
- `src/providers/graphql-provider.tsx` must attach OAuth headers for every request:
47
-
48
- - Accept `authClient` as a prop
49
- - Call `authClient.getAuthHeadersForQuery()`
50
- - Set both headers:
51
- - `Authorization`
52
- - `DPoP`
53
- - Keep `Content-Type: application/json`
54
-
55
- Do not use unauthenticated static headers for `/query`.
@@ -1,86 +0,0 @@
1
- ---
2
- paths:
3
- - "examples/**/src/pages/**/page.tsx"
4
- ---
5
-
6
- # Page File Structure (File-Based Routing)
7
-
8
- Each `page.tsx` file should contain a default-exported page component with optional `appShellPageProps` static field.
9
- Pages are automatically discovered by the Vite plugin.
10
-
11
- ## Path Convention
12
-
13
- The URL path is derived from the directory structure:
14
-
15
- ```
16
- src/pages/
17
- ├── page.tsx # / (root path)
18
- ├── purchasing/
19
- │ ├── page.tsx # /purchasing
20
- │ └── orders/
21
- │ ├── page.tsx # /purchasing/orders
22
- │ └── [id]/
23
- │ └── page.tsx # /purchasing/orders/:id
24
- └── (admin)/ # Grouping (not included in path)
25
- └── settings/
26
- └── page.tsx # /settings
27
- ```
28
-
29
- | Directory Name | Converts To | Description |
30
- | -------------- | ----------- | ----------------------------- |
31
- | `orders` | `orders` | Static segment |
32
- | `[id]` | `:id` | Dynamic parameter |
33
- | `(group)` | (excluded) | Grouping only (not in path) |
34
- | `_lib` | (ignored) | Not routed (for shared logic) |
35
-
36
- ## Page Component Pattern
37
-
38
- ```tsx
39
- import { Layout, type AppShellPageProps } from "@tailor-platform/app-shell";
40
- import { useQuery } from "urql";
41
- import { graphql } from "@/graphql";
42
- import { ErrorFallback } from "@/components/composed/error-fallback";
43
- import { Loading } from "@/components/composed/loading";
44
-
45
- const MyQuery = graphql(`...`);
46
-
47
- const MyPage = () => {
48
- const [{ data, error, fetching }, reexecuteQuery] = useQuery({
49
- query: MyQuery,
50
- });
51
-
52
- if (fetching) return <Loading />;
53
-
54
- if (error || !data) {
55
- return (
56
- <ErrorFallback
57
- title="Failed to load"
58
- message="An error occurred while fetching data."
59
- onReset={() => reexecuteQuery({ requestPolicy: "network-only" })}
60
- />
61
- );
62
- }
63
-
64
- return (
65
- <Layout columns={1} title="My Page">
66
- <Layout.Column>
67
- <MyComponent data={data} />
68
- </Layout.Column>
69
- </Layout>
70
- );
71
- };
72
-
73
- MyPage.appShellPageProps = {
74
- meta: { title: "My Page" },
75
- } satisfies AppShellPageProps;
76
-
77
- export default MyPage;
78
- ```
79
-
80
- ## Key Points
81
-
82
- - Handle `fetching` state with `<Loading />`
83
- - Handle `error || !data` with `<ErrorFallback />` and `reexecuteQuery` for retry
84
- - Use `appShellPageProps` static field for metadata (title, icon) and guards
85
- - Guards on parent pages are automatically inherited by child pages
86
- - See `component.md` for fragment collocation
@@ -1,28 +0,0 @@
1
- ---
2
- paths:
3
- - "modules/*/src/db/*.ts"
4
- - "modules/*/src/module.ts"
5
- ---
6
-
7
- # Cross-Module Type Injection
8
-
9
- ## Typing External Module References
10
-
11
- - Derive types from the source module's `defineModule` return type, never use `any`
12
- - Use `import type` for type-only imports
13
- - All modules live in the same package (`@tailor-platform/erp-kit`), so use relative paths
14
- - Define a local type alias for readability: `type UnitType = ReturnType<typeof definePrimitivesModule>["unit"]`
15
-
16
- ## DB Type Creator Pattern (`src/db/*.ts`)
17
-
18
- - Accept optional external type via `Create*TypeParams` (e.g., `unitType?: UnitType`)
19
- - Use a ternary on the param to conditionally add `.relation()`:
20
- - **Present**: `db.uuid().relation({ type: "n-1", toward: { type: param }, backward: "..." })`
21
- - **Absent**: `db.uuid()` (plain UUID, no relation) — preserves standalone usage
22
- - Export a default instance at file bottom with `{}` params for internal/standalone use
23
-
24
- ## Module Wiring (`src/module.ts`)
25
-
26
- - Group external dependencies under a `primitives?` key in `DefineModuleParams`
27
- - Spread consumer's `Create*TypeParams` and merge the injected type: `{ ...params.productTemplate, unitType: params.primitives?.unit }`
28
- - Export `DefineModuleParams` type from `index.ts` so consumers can type their config
@@ -1,24 +0,0 @@
1
- ---
2
- paths:
3
- - "modules/*/src/"
4
- ---
5
-
6
- # Dependency Modules
7
-
8
- When a module depends on another module, create `modules/<module-name>/src/dep.ts` to instantiate and re-export the dependency:
9
-
10
- ```typescript
11
- import { defineModule } from "../../primitives/src/module";
12
-
13
- const primitives = defineModule();
14
-
15
- // Re-export db types for use in this module's db definitions and commands
16
- export const { unit, currency } = primitives.db;
17
- ```
18
-
19
- ## Module Return Structure
20
-
21
- `defineModule` returns an object with:
22
-
23
- - `db`: Object of database model types (keyed by name)
24
- - `executors`: Object of executor instances (keyed by name, if any)
@@ -1,67 +0,0 @@
1
- ---
2
- paths:
3
- - "modules/*/src/executor/"
4
- - "modules/*/src/module.ts"
5
- ---
6
-
7
- # Executors
8
-
9
- Executors handle asynchronous operations triggered by database record changes.
10
-
11
- ## Factory Pattern
12
-
13
- Executors are **factory functions** that accept configuration and return an executor:
14
-
15
- ```typescript
16
- export const myExecutor = function myExecutor({ namespace }: { namespace: string }) {
17
- return createExecutor({
18
- name: "myExecutor",
19
- // ... executor config
20
- });
21
- };
22
- ```
23
-
24
- **Why factory functions:**
25
-
26
- - Executors need runtime configuration (db namespace) not known at import time
27
- - Named function expression enables better stack traces
28
- - Module consumers control configuration via `defineModule` params
29
-
30
- ## File Organization
31
-
32
- - Place in `src/executor/` directory
33
- - Group related executors in one file (e.g., `recomputeOnRolePermissionChange.ts`)
34
- - Name files after the operation, not the trigger
35
-
36
- ## Executor Structure
37
-
38
- Required fields:
39
-
40
- - `name`: Matches function name exactly
41
- - `description`: Human-readable purpose
42
- - `trigger`: `recordCreatedTrigger` or `recordDeletedTrigger` with `type`
43
- - `operation`: `kind: "jobFunction"` with async `body`
44
-
45
- ## Database Access
46
-
47
- Use namespace parameter with `getDB`:
48
-
49
- ```typescript
50
- // @ts-expect-error unsure at build time
51
- const db = getDB(namespace);
52
- ```
53
-
54
- The `@ts-expect-error` comment is required because the namespace is validated at runtime.
55
-
56
- ## Module Integration
57
-
58
- `defineModule` returns executors in a dedicated array:
59
-
60
- ```typescript
61
- return {
62
- db: [user, role, ...],
63
- executors: [rolePermissionCreated, rolePermissionDeleted],
64
- };
65
- ```
66
-
67
- Instantiate executors in `module.ts` with the `dbNamespace` parameter.
@@ -1,83 +0,0 @@
1
- ---
2
- paths:
3
- - "modules/*/src/"
4
- ---
5
-
6
- # Sync vs Async Operations
7
-
8
- ## Decision Criteria
9
-
10
- Choose synchronous or asynchronous execution based on:
11
-
12
- 1. **Number of affected records** - How many records need processing?
13
- 2. **Data growth trajectory** - Will the operation slow down as data grows?
14
-
15
- | Scenario | Approach | Implementation |
16
- | ----------------------------------- | ------------------ | --------------------------- |
17
- | Single record, bounded complexity | **Synchronous** | Inline in command |
18
- | Single record, unbounded complexity | **Consider async** | Evaluate growth |
19
- | Multiple records | **Asynchronous** | Executor with `jobFunction` |
20
-
21
- ## Growth Considerations
22
-
23
- Ask: "How does this operation scale as the system grows?"
24
-
25
- - **Bounded**: User status update (always 1 record, O(1))
26
- - **Bounded**: Single user permission recompute (limited by reasonable role/permission counts)
27
- - **Unbounded**: All users with role X (grows with user base, O(n))
28
- - **Unbounded**: All orders in date range (grows with transaction volume)
29
-
30
- When in doubt, monitor operation timing in production and migrate to async if latency increases.
31
-
32
- ## Rationale
33
-
34
- - **Synchronous**: Acceptable when operation time is predictable and bounded
35
- - **Asynchronous**: Required when operation time scales with data volume
36
-
37
- ## Pattern: Synchronous (Single Record)
38
-
39
- Commands that affect a single known record should execute the operation inline and return the result:
40
-
41
- ```typescript
42
- // In command - recompute immediately
43
- const updatedUser = await recomputeUserPermissions(db, input.userId);
44
- return { userRole, user: updatedUser, auditEvent };
45
- ```
46
-
47
- ## Pattern: Asynchronous (Multiple Records)
48
-
49
- Commands that affect an unknown number of records should delegate to an executor:
50
-
51
- ```typescript
52
- // Command just creates/deletes the record
53
- // Executor handles recomputation asynchronously
54
- ```
55
-
56
- Create executors with record triggers:
57
-
58
- - `recordCreatedTrigger` - React to inserts
59
- - `recordDeletedTrigger` - React to deletes
60
- - Use `kind: "jobFunction"` for extended execution
61
-
62
- ## Example: Permission Recomputation
63
-
64
- | Command | Affected | Approach |
65
- | ------------------------ | ----------------------- | --------------- |
66
- | assignRoleToUser | 1 user | Sync in command |
67
- | revokeRoleFromUser | 1 user | Sync in command |
68
- | assignPermissionToRole | N users (all with role) | Async executor |
69
- | revokePermissionFromRole | N users (all with role) | Async executor |
70
-
71
- ## Trade-offs
72
-
73
- **Synchronous:**
74
-
75
- - Immediate consistency
76
- - Simpler error handling
77
- - Blocks request until complete
78
-
79
- **Asynchronous:**
80
-
81
- - Eventual consistency
82
- - Non-blocking requests
83
- - Requires executor infrastructure
@@ -1,14 +0,0 @@
1
- ---
2
- paths:
3
- - "modules/*/src/db/*.ts"
4
- - "modules/*/src/executor/*.ts"
5
- - "modules/*/src/workflow/*.ts"
6
- ---
7
-
8
- # SDK Docs Reference
9
-
10
- Read the official Tailor SDK documentation for database models, executors:
11
-
12
- - [TailorDB Models](https://raw.githubusercontent.com/tailor-platform/sdk/refs/heads/main/packages/sdk/docs/services/tailordb.md)
13
- - [Executors](https://raw.githubusercontent.com/tailor-platform/sdk/refs/heads/main/packages/sdk/docs/services/executors.md)
14
- - [Workflow](https://raw.githubusercontent.com/tailor-platform/sdk/refs/heads/main/packages/sdk/docs/services/workflow.md)