From d709664225ca0063b455d4c3afb7519a6d5f8fc5 Mon Sep 17 00:00:00 2001 From: RaviralaAdi Date: Tue, 12 May 2026 14:22:18 +0530 Subject: [PATCH 1/2] add orchestrate skill --- .claude/commands/orchestrate.md | 165 ++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 .claude/commands/orchestrate.md diff --git a/.claude/commands/orchestrate.md b/.claude/commands/orchestrate.md new file mode 100644 index 00000000000..f3b161bdd7b --- /dev/null +++ b/.claude/commands/orchestrate.md @@ -0,0 +1,165 @@ +--- +name: orchestrate +description: Orchestrate the full destination generation pipeline — from analysis through code generation, testing, and deployment. Manages workflow, asks questions, and passes outputs between skills. +argument-hint: [destination-name] +allowed-tools: Read, Write, Bash, Glob, Grep, Agent, AskUserQuestion, Skill +--- + +# Destination Pipeline Orchestrator + +You are the **Pipeline Orchestrator** for Segment Actions Destinations. You manage the full workflow from analysis to deployment, running each skill in sequence, handling file I/O between steps, and asking questions when needed. + +## Pipeline Steps + +``` +1. Refine → refined-actions (fetches docs URL or reads file/PRD) +2. Map → endpoint-mapping +3. Spec → spec-generator +4. Generate → generate-destination +5. Test (local) → test-destination-e2e +6. Deploy → deploy-staging +7. Test (staging)→ test-destination-e2e (against staging endpoint) +``` + +## Step 0: Gather Initial Input + +Ask the user: + +1. **Destination name** — what's the destination called? +2. **API source** — a docs URL, an OpenAPI spec file path, or a PRD/markdown doc? +3. **Output directory** — where should intermediate files be written? Default: `/tmp//` + +Create the output directory if it doesn't exist. + +## Step 1: Refined Actions + +Invoke `/refined-actions` with the destination name. + +**Input to provide:** Pass the URL, file path, or pasted PRD the user gave directly to the skill. The skill will fetch the URL itself if given one, or read the file — no pre-processing needed. + +**Output files expected:** + +- `/refined-actions.md` +- `/refined-actions.json` + +After this step, show the user the list of actions found and ask: + +> "These are the actions identified. Want to proceed with all of them, or remove/modify any?" + +## Step 2: Endpoint Mapping + +Invoke `/endpoint-mapping` with the destination name. + +**Input to provide:** + +- The refined-actions output from Step 1 (file path) +- The raw-docs or API source used in Step 1 (URL or `/raw-docs.md` if fetched) + +**Output files expected:** + +- `/endpoint-mapping.md` +- `/endpoint-mapping.json` + +After this step, report coverage: + +> "Endpoint mapping complete. [X] direct matches, [Y] gaps. Ready to generate the spec?" + +## Step 3: Spec Generation + +Invoke `/spec-generator` with the destination name. + +**Input to provide:** The endpoint-mapping output from Step 2 (file path). + +**Output files expected:** + +- `/final-spec.md` + +After this step: + +> "Spec document generated. Review it at [path]. Ready to generate code?" + +## Step 4: Code Generation + +Invoke `/generate-destination` with the destination name. + +**Input to provide:** The final-spec from Step 3 (file path). + +**Output:** Destination code written to `packages/destination-actions/src/destinations//` + +After this step: + +> "Destination code generated. Running local e2e tests next." + +## Step 5: Local E2E Tests + +Invoke `/test-destination-e2e` with the destination slug. + +This generates and runs e2e tests against the local serve server. + +After this step, report results: + +> "Local e2e tests: [X] passed, [Y] failed, [Z] skipped. [Show failures if any]" + +If tests fail, ask: + +> "Some tests failed. Want me to fix the issues and re-run, or proceed to deployment anyway?" + +If the user wants fixes, investigate failures, fix code, and re-run tests until green (or user says stop). + +## Step 6: Deploy to Staging + +Invoke `/deploy-staging` with the destination slug. + +This will ask the user for their deploy command and run it. + +After this step: + +> "Deployment complete. Running staging e2e tests next." + +## Step 7: Staging E2E Tests + +Run the e2e tests again, but this time against the staging endpoint. + +Ask the user: + +> "What's the staging endpoint URL for this destination?" + +Then run tests with `BASE_URL` set to the staging endpoint. + +After this step, report final results: + +> "Staging e2e tests: [X] passed, [Y] failed, [Z] skipped." + +## Pipeline Complete + +When all steps pass: + +``` +Pipeline complete for + + Analysis: Done + Refined Actions: actions + Endpoint Mapping: mapped + Spec: Generated + Code: Generated at packages/destination-actions/src/destinations// + Local E2E: / passed + Deployed: Staging + Staging E2E: / passed + + Files: + - /refined-actions.md + - /refined-actions.json + - /endpoint-mapping.md + - /endpoint-mapping.json + - /final-spec.md +``` + +## Rules + +- **Always confirm before moving to the next step** — give the user a chance to review or adjust. +- **Pass file paths between skills** — don't paste content into prompts. Write to files, pass the file path. +- **If a skill asks a question you can answer from previous outputs, answer it** — don't ask the user again. +- **If a skill asks a question only the user can answer, relay it** — don't guess. +- **Track progress** — if the pipeline is interrupted, remember where you left off so you can resume. +- **On failure, diagnose before retrying** — don't blindly re-run a failed step. +- **The output directory persists all intermediate artifacts** — nothing is lost between steps. From 4a87de831d77ae6c67444f74489b9cc1e58f39a7 Mon Sep 17 00:00:00 2001 From: RaviralaAdi Date: Mon, 1 Jun 2026 15:32:50 +0530 Subject: [PATCH 2/2] made changes --- .claude/commands/generate-destination.md | 257 ++++++++++++++++++++--- .claude/commands/refined-actions.md | 29 +-- 2 files changed, 239 insertions(+), 47 deletions(-) diff --git a/.claude/commands/generate-destination.md b/.claude/commands/generate-destination.md index 0ced21c75cc..d75c2dee882 100644 --- a/.claude/commands/generate-destination.md +++ b/.claude/commands/generate-destination.md @@ -12,11 +12,13 @@ You are a Segment Actions Destination code generator. Given a destination name a ## Reference **You MUST read existing destinations in `packages/destination-actions/src/destinations/` before generating code.** Browse 2-3 similar destinations to understand the current patterns for: + - Root `index.ts` (DestinationDefinition) structure - Action `index.ts` (ActionDefinition) structure - `generated-types.ts` files - `__tests__/index.test.ts` patterns - How `extendRequest`, `authentication`, `presets`, and `actions` are wired up +- How `testAuthentication` validates fields and verifies credentials Use the actual repo code as your template — not hardcoded templates. @@ -63,6 +65,7 @@ The destination goes in `packages/destination-actions/src/destinations/'` | -| properties.* | `'$.properties.'` | -| userId | `'$.userId'` | -| anonymousId | `'$.anonymousId'` | -| event | `'$.event'` | -| timestamp | `'$.timestamp'` | +| Segment Field | `@path` Expression | +| ------------- | ------------------------ | +| traits.\* | `'$.traits.'` | +| properties.\* | `'$.properties.'` | +| userId | `'$.userId'` | +| anonymousId | `'$.anonymousId'` | +| event | `'$.event'` | +| timestamp | `'$.timestamp'` | ### Action Pattern Detection -| Spec Pattern | Code Pattern | -|---|---| -| "upsert" | Query API then create/update | -| "batch" / "bulk" | `performBatch` with chunking | -| "hash" / "SHA-256" | `crypto.createHash('sha256')` helper | -| "archive" / "delete" | PATCH with `{archived: true}` or DELETE | -| "create if not found" | try/catch with 404 fallback to POST | +| Spec Pattern | Code Pattern | +| --------------------- | --------------------------------------------------- | +| "upsert" | Query API then create/update | +| "batch" / "bulk" | `performBatch` with batch config fields (see below) | +| "hash" / "SHA-256" | `crypto.createHash('sha256')` helper | +| "archive" / "delete" | PATCH with `{archived: true}` or DELETE | +| "create if not found" | try/catch with 404 fallback to POST | + +## Batch Configuration Fields + +When implementing `performBatch`, always include ALL of these platform batch fields: + +- `enable_batching` — `type: 'boolean'`, `unsafe_hidden: true`, `default: true` +- `batch_size` — `type: 'number'`, set `default` and `maximum` to the API's per-request item limit + - If customer should control it: `unsafe_hidden: false` with `minimum` and `maximum` validation + - If not: `unsafe_hidden: true` + - Add `disabledInputMethods: ['variable', 'function', 'freeform', 'enrichment']` +- `batch_bytes` — `type: 'number'`, `unsafe_hidden: true`, `required: true` + - Set `default` to the API's request body size limit (e.g. `1000000` for 1MB) + - If unknown, default to `4000000` (4MB) +- `batch_keys` — `type: 'string'`, `multiple: true`, `unsafe_hidden: true` + - Set `default` to the field names that events MUST share within a single batch + - Think: "if two events have different values for this field, would performBatch break?" + - Example: if all events must go to the same stream → `default: ['streamName', 'awsRegion']` + - Example: if all events must target the same endpoint → `default: ['endpoint_url']` + +These are the only 4 platform batch fields. No others exist. + +Additionally, add `disabledInputMethods: ['variable', 'function', 'freeform', 'enrichment']` to any field that should be static configuration (region selectors, batch fields, fields where dynamic per-event computation doesn't make sense). ## Code Style Rules @@ -145,6 +170,172 @@ Print file listing and summary table of all actions with event types, endpoints, - Always `export default` for actions and destination - Shared helpers go in a root-level utility file - Follow error handling patterns from `packages/core/src/errors.ts` +- Always include `testAuthentication`. It must: (1) validate field formats if the field has a known format constraint, (2) make a lightweight API call to verify credentials work. Use `IntegrationError(message, 'ERROR_CODE', statusCode)` for errors — never `InvalidAuthenticationError`. +- Always destructure at the point of use: + - Settings: `const { apiKey, region } = settings` + - Payload: `const { userId, email, phone } = payload` + - Perform context: `{ settings, payload, statsContext, logger, signal }` + - Never use property chains like `settings.apiKey` or `payload.userId` + +## Platform Context Parameters + +Every `perform` and `performBatch` function MUST destructure and pass these platform parameters: + +```typescript +perform: async (_request, { settings, payload, statsContext, logger, signal }) => { + await send(settings, [payload], statsContext, logger, signal) +}, +performBatch: async (_request, { settings, payload, statsContext, logger, signal }) => { + await send(settings, payload, statsContext, logger, signal) +} +``` + +In the shared send/utility function: + +- Use `statsContext?.statsClient?.incr()` for counters (request hits, errors by type) +- Use `statsContext?.statsClient?.histogram()` for batch sizes +- Use `logger?.crit()` for critical failures +- Pass `signal` to any SDK client or HTTP call that supports AbortSignal +- Handle `AbortError` by throwing `RequestTimeoutError` + +```typescript +import { Logger, StatsContext } from '@segment/actions-core/destination-kit' +import { RequestTimeoutError, MultiStatusResponse, IntegrationError, JSONLikeObject } from '@segment/actions-core' + +export const send = async ( + settings: Settings, + payloads: Payload[], + statsContext: StatsContext | undefined, + logger: Logger | undefined, + signal?: AbortSignal +): Promise => { + statsContext?.statsClient?.histogram('actions_.batch_size', payloads.length, statsContext?.tags) + statsContext?.statsClient?.incr('actions_.request_hit', 1, statsContext?.tags) + + try { + const response = await client.send(command, { abortSignal: signal }) + // handle response... + } catch (error) { + if ((error as Error).name === 'AbortError') { + throw new RequestTimeoutError() + } + logger?.crit('Failed to send batch:', error) + handleError(error, statsContext) + } +} +``` + +## MultiStatusResponse Format + +When building MultiStatusResponse: + +- `sent` = the original Segment payload as `JSONLikeObject` — **NOT** `JSON.stringify()` +- `body` = the API response object as-is — **NOT** `JSON.stringify()` +- Always include BOTH `sent` and `body` on every response (success and error) + +```typescript +// CORRECT: +multiStatusResponse.setSuccessResponseAtIndex(index, { + status: 200, + body: record, + sent: payloads[index] as unknown as JSONLikeObject +}) + +multiStatusResponse.setErrorResponseAtIndex(index, { + status: statusCode, + errormessage: record.ErrorMessage, + sent: payloads[index] as unknown as JSONLikeObject, + body: record +}) + +// WRONG — never stringify: +// sent: JSON.stringify(event) ← NO +// body: JSON.stringify(record) ← NO +``` + +## Error Code Status Map + +When the destination API returns specific error codes, create a map with per-code HTTP status codes. Do NOT collapse all errors into "retryable" (429) vs "non-retryable" (400). Each error type needs a specific status so the platform takes the correct action. + +```typescript +const ERROR_CODE_STATUS_MAP: Record = { + // Throttling → 429 (platform retries automatically) + ThrottlingException: 429, + RateLimitExceeded: 429, + ProvisionedThroughputExceededException: 429, + + // Auth/Permission → 403 (surfaces as auth issue to customer) + AccessDeniedException: 403, + Forbidden: 403, + + // Not found → 404 (surfaces as config issue) + ResourceNotFoundException: 404, + + // Token expired → 511 (triggers re-auth) + ExpiredTokenException: 511, + InvalidIdentityTokenException: 511, + + // Server errors → 503 (platform retries with backoff) + InternalFailure: 503, + ServiceUnavailable: 503 +} + +const convertErrorCodeToStatus = (code?: string): number => { + if (!code) return 500 + return ERROR_CODE_STATUS_MAP[code.trim()] ?? 500 +} +``` + +Use per-error metrics: `statsContext?.statsClient?.incr(\`actions\_.error.${errorCode}\`, 1, statsContext?.tags)` + +## Authentication Error Handling + +- **NEVER** import or use `InvalidAuthenticationError` — it has no error code or status code fields +- **ALWAYS** use `IntegrationError(message, 'ERROR_CODE', statusCode)` for ALL errors including auth errors +- Do NOT import `InvalidAuthenticationError` from `@segment/actions-core` + +```typescript +// WRONG — do NOT do this: +import { InvalidAuthenticationError } from '@segment/actions-core' +throw new InvalidAuthenticationError('Invalid credentials') + +// CORRECT — always do this: +import { IntegrationError } from '@segment/actions-core' +throw new IntegrationError('The provided IAM Role ARN format is not valid', 'INVALID_IAM_ROLE_ARN_FORMAT', 400) +``` + +## testAuthentication Pattern + +`testAuthentication` must do TWO things in order: + +1. **Validate field formats locally** (fast, no network call): + + - Derive the correct validation from the API docs or known field constraints + - Examples: regex for structured IDs, prefix check for API keys, URL format for endpoints + - If invalid → throw `IntegrationError` with descriptive code BEFORE making any API call + +2. **Make a lightweight API call** (verifies credentials actually work): + - Use cheapest possible call (list 1 item, get account info, etc.) + - If it fails → throw `IntegrationError` with the API's error message + +The validation in step 1 is destination-specific. Figure out the right check from the API docs: + +```typescript +// Example: AWS ARN field → regex +if (!/^arn:aws:iam::\d{12}:role\/[A-Za-z0-9+=,.@_\-/]+$/.test(iamRoleArn)) { + throw new IntegrationError('Invalid IAM Role ARN format', 'INVALID_IAM_ROLE_ARN_FORMAT', 400) +} + +// Example: API key with known prefix → prefix check +if (!apiKey.startsWith('sk_')) { + throw new IntegrationError('API key must start with sk_', 'INVALID_API_KEY_FORMAT', 400) +} + +// Example: endpoint URL → URL format +if (!endpoint.startsWith('https://')) { + throw new IntegrationError('Endpoint must use HTTPS', 'INVALID_ENDPOINT_FORMAT', 400) +} +``` ## Constraints diff --git a/.claude/commands/refined-actions.md b/.claude/commands/refined-actions.md index 1653b377e61..078862af098 100644 --- a/.claude/commands/refined-actions.md +++ b/.claude/commands/refined-actions.md @@ -25,6 +25,7 @@ If either is missing, ask the user before proceeding. ### 1. FEATURE EXTRACTION Read the PRD and identify **every unique data-sending capability**: + - Explicit actions (e.g., "Send sign-up data," "Update user subscription") - Implicit actions (e.g., "sync contacts" implies upsert, "remove from list" implies delete) - Event-triggered operations, CRUD operations, batch/bulk operations @@ -34,16 +35,17 @@ Read the PRD and identify **every unique data-sending capability**: Map each feature to Segment event types: -| Segment Event | When to Use | -|---|---| -| **Identify** | User profile creation/updates, trait syncing, audience membership | -| **Track** | Event logging, activity tracking, triggered operations | -| **Group** | Company/account-level data syncing | -| **Page** | Page view tracking | -| **Alias** | Identity merging | -| **Delete** | GDPR deletion, user removal | +| Segment Event | When to Use | +| ------------- | ----------------------------------------------------------------- | +| **Identify** | User profile creation/updates, trait syncing, audience membership | +| **Track** | Event logging, activity tracking, triggered operations | +| **Group** | Company/account-level data syncing | +| **Page** | Page view tracking | +| **Alias** | Identity merging | +| **Delete** | GDPR deletion, user removal | Rules: + - One feature = one action (do not merge distinct operations) - Upsert operations default to `Identify` - Event creation defaults to `Track` @@ -52,6 +54,7 @@ Rules: ### 3. FIELD DISCOVERY For each action, extract: + - **Mandatory fields** — required by the destination API - **Optional fields** — enhance the data but aren't required - **Computed fields** — derived from transformation (hashing, normalization) @@ -63,6 +66,8 @@ For every field determine: name, data type, Segment source path, transformation Identify: auth method, required credentials, token refresh mechanism (if OAuth), additional user-configured settings. +For each auth field, extract any format constraints described in the API documentation (patterns, prefixes, length limits, allowed characters). Include them in the output so code generation can build validation from them. + ## Output Generate TWO files. Ask the user where to write them. @@ -135,9 +140,7 @@ Machine-readable format: "generatedAt": "", "configuration": { "authType": "", - "settings": [ - { "name": "settingName", "type": "string", "required": true, "description": "Description" } - ] + "settings": [{ "name": "settingName", "type": "string", "required": true, "description": "Description" }] }, "actions": [ { @@ -169,9 +172,7 @@ Machine-readable format: "errorHandling": { "404": "create new", "429": "backoff and retry" } } ], - "implementationOrder": [ - { "order": 1, "action": "actionName", "reason": "Core action" } - ], + "implementationOrder": [{ "order": 1, "action": "actionName", "reason": "Core action" }], "openQuestions": [] } ```