# Tray CDK — Complete Guide for Claude Code > **Purpose:** This is a step-by-step reference for Claude Code to build, test, and deploy Tray CDK connectors. It covers every CLI operation, every file you'll touch, the exact order of operations, and how to handle common problems. --- ## Table of Contents 1. [Understanding Tray CDK](#1-understanding-tray-cdk) 2. [Environment Setup](#2-environment-setup) 3. [CLI Command Reference](#3-cli-command-reference) 4. [End-to-End Development Flow](#4-end-to-end-development-flow) 5. [Project Structure Deep Dive](#5-project-structure-deep-dive) 6. [Authentication Configuration](#6-authentication-configuration) 7. [Global Configuration](#7-global-configuration) 8. [Building HTTP Operations](#8-building-http-operations) 9. [Building Composite Operations](#9-building-composite-operations) 10. [Dynamic Dropdown Lists (DDLs)](#10-dynamic-dropdown-lists-ddls) 11. [Input Configuration & UI Annotations](#11-input-configuration--ui-annotations) 12. [Output Configuration](#12-output-configuration) 13. [Testing Operations](#13-testing-operations) 14. [File Handling](#14-file-handling) 15. [Deployment](#15-deployment) 16. [Permissions & Sharing](#16-permissions--sharing) 17. [Typical Flows](#17-typical-flows) 18. [Troubleshooting](#18-troubleshooting) --- ## 1. Understanding Tray CDK The Tray Connector Development Kit (CDK) is a TypeScript-based toolkit for building custom connectors on the Tray.ai integration platform. Connectors expose third-party API operations as reusable steps in Tray workflows. **Core Concepts:** - **Connector** — A package containing one or more operations for a specific service (e.g., a Slack connector, a CRM connector). - **Operation** — A single action the connector can perform (e.g., "Get Users", "Create Contact"). - **Handler** — The logic behind an operation. Two types: `http` (single API call) or `composite` (multi-step, data processing, DDLs). - **DSL** — The declarative domain-specific language (`@trayio/cdk-dsl`) used to define connector behaviour. You describe what the connector does; the Tray runtime executes it. - **Namespace** — An org-level prefix for all your connectors (e.g., `acme-corp`). One per organization. - **Service** — A Tray-platform entity that handles auth (OAuth dance, token refresh). Created in the Tray UI, referenced from `connector.json`. **When to use HTTP vs Composite:** | Use `http` when... | Use `composite` when... | |---|---| | The operation makes exactly ONE HTTP call | The operation makes MULTIPLE HTTP calls | | Standard REST/SOAP/GraphQL endpoint | You need to invoke other operations | | Response can be returned directly or with simple transform | You need a DDL (dynamic dropdown) | | | You use npm packages for data processing | | | You need conditional logic between API calls | --- ## 2. Environment Setup ### Prerequisites ```bash # Required: Node.js v18+ node --version # Must be >= 18 # Install CDK CLI globally npm install -g @trayio/cdk-cli # Verify installation tray-cdk version ``` ### Environment Variables These are needed at various stages: ```bash # For namespace operations and deployment export TRAY_API_TOKEN= # For deployment (region-specific) export TRAY_API_BASE_URL=https://api.tray.io # US # export TRAY_API_BASE_URL=https://api.eu1.tray.io # EU # export TRAY_API_BASE_URL=https://api.ap1.tray.io # APAC ``` The RBAC token is created in the Tray UI under Organization settings → API users & tokens. --- ## 3. CLI Command Reference ### 3.1 Informational Commands #### `tray-cdk version` Displays CLI version, architecture, and Node version. ```bash tray-cdk version # @trayio/cdk-cli/5.14.0 darwin-arm64 node-v20.9.0 tray-cdk version --verbose # Includes plugin versions tray-cdk version --json # Machine-readable output ``` **Use when:** Verifying installation, debugging environment issues, reporting bugs. --- #### `tray-cdk help [COMMAND]` Shows help for any command. ```bash tray-cdk help # List all commands tray-cdk help connector # Help for connector subcommands tray-cdk help connector init # Help for specific command tray-cdk help --nested-commands # Include all subcommands ``` **Use when:** You need to check syntax, flags, or available subcommands. --- #### `tray-cdk autocomplete [SHELL]` Sets up tab-completion for your shell. ```bash tray-cdk autocomplete bash tray-cdk autocomplete zsh tray-cdk autocomplete powershell tray-cdk autocomplete --refresh-cache # After CLI update ``` **Use when:** Once after installation, and again after CLI updates. --- ### 3.2 Namespace Commands #### `tray-cdk namespace create ORGID NAMESPACE` Creates a connector namespace for your organization. **One-time operation per org.** ```bash tray-cdk namespace create my-company --us ``` - `ORGID` — Found in URL: `https://app.tray.io/workspaces/[UUID]` - `NAMESPACE` — Short, lowercase, hyphens OK, no numbers/symbols. e.g., `acme-corp` - Region flags: `--us`, `--eu`, `--ap` **Naming rules:** - Associate with your company's brand name - Prefer shorter names - Hyphens allowed, numbers and other symbols are not - Same namespace can be shared across orgs in different regions **Use when:** Before your first-ever connector deployment. --- #### `tray-cdk namespace get ORGID` Retrieves the existing namespace for an organization. ```bash tray-cdk namespace get --us ``` **Use when:** You forgot the namespace, or verifying it exists before deployment. --- ### 3.3 Connector Development Commands #### `tray-cdk connector init [CONNECTOR_NAME]` Scaffolds a new connector project with all boilerplate files and a sample operation. ```bash tray-cdk connector init my-service # Create project, then npm install manually tray-cdk connector init my-service -i # Create and install dependencies in one step ``` **What it creates:** ``` my-service/ ├── connector.json ├── package.json ├── jest.config.js ├── tsconfig.json └── src/ ├── Auth.ts # Default: `never` auth type ├── GlobalConfig.ts # Default: empty global config ├── test.ctx.json # Empty test context └── get_post/ # Sample operation (DELETE THIS) ├── operation.json ├── input.ts ├── output.ts ├── handler.ts └── handler.test.ts ``` **Use when:** Starting a brand-new connector from scratch. **Important:** Always delete the sample `get_post/` operation folder before building your real operations. --- #### `tray-cdk connector import [OPENAPI_SPEC] [CONNECTOR_NAME]` Auto-generates a connector from an OpenAPI 3.0 JSON specification. ```bash tray-cdk connector import ./api-spec.json my-connector ``` **Requirements:** - JSON format only (not YAML) - OpenAPI version 3.0 (not Swagger 2.0) - Every endpoint must have a unique `operationId` **If errors occur:** An `errors.json` file is generated in the output directory. **Use when:** The third-party service provides an OpenAPI spec and you want to bootstrap quickly. You still need to configure Auth.ts, GlobalConfig.ts, and write tests afterward. --- #### `tray-cdk connector add-operation [OPERATION_NAME] [OPERATION_TYPE]` Adds a new operation to an existing connector project. **Must be run from the connector's root directory.** ```bash # Add an HTTP operation (single API call) tray-cdk connector add-operation get_users http # Add a composite operation (multi-step / DDL / utility) tray-cdk connector add-operation convert_json_to_xml composite ``` - `OPERATION_NAME` — snake_case name for the operation - `OPERATION_TYPE` — Either `http` or `composite` **What it creates (under `src//`):** - `operation.json` — `{"name": "", "title": ""}` - `input.ts` — Empty input type - `output.ts` — Empty output type - `handler.ts` — Skeleton handler of the chosen type - `handler.test.ts` — Skeleton test **Decision guide for OPERATION_TYPE:** | Scenario | Type | Why | |---|---|---| | GET /users endpoint | `http` | Single API call | | POST /contacts with JSON body | `http` | Single API call | | PUT /records/:id | `http` | Single API call | | DELETE /items/:id | `http` | Single API call | | Upsert (read, then create or update) | `composite` | Multiple API calls + conditional logic | | DDL dropdown (list options from API) | `composite` | Returns `{text, value}` pairs | | JSON → XML conversion | `composite` | No API call, uses npm package | | SHA-256 hash generation | `composite` | No API call, uses crypto library | | Report combining 3 API endpoints | `composite` | Multiple API calls | | HTML → Markdown conversion | `composite` | Uses turndown npm package | **Use when:** Every time you need to add functionality to your connector. --- #### `tray-cdk connector test [OPERATION_NAME]` Builds the connector and runs tests using Jest. ```bash # Test ALL operations tray-cdk connector test # Test a specific operation tray-cdk connector test get_users # Test with verbose output (shows input/output) tray-cdk connector test get_users -v # Alternative: use npm directly npm test ``` **What happens internally:** 1. TypeScript compilation 2. Schema generation from input/output types 3. Jest test execution using `jest.config.js` 4. Tests use `test.ctx.json` for auth context **Use when:** After writing or modifying any operation. Always test before deployment. --- #### `tray-cdk connector build` Builds the connector without running tests. Generates JSON schemas and the deployment artifact. ```bash tray-cdk connector build ``` **What happens:** 1. Compiles TypeScript 2. Converts `input.ts` / `output.ts` types → JSON Schema 3. Generates `.tray/connector.zip` **Use when:** You want to inspect generated JSON schemas, or as a pre-deployment step in CI/CD. --- ### 3.4 Deployment Commands #### `tray-cdk deployment create` Runs tests, builds, and deploys the connector to the Tray platform. ```bash export TRAY_API_TOKEN= export TRAY_API_BASE_URL=https://api.tray.io # US region tray-cdk deployment create ``` **What happens:** 1. Tests run first (deployment aborts if tests fail) 2. Connector is built 3. Artifact is uploaded to Tray 4. Returns a deployment UUID for status polling **Console output on success:** ``` ...Test execution logs... Tests ran successfully Connector Build Started Generating schemas for operation get_users Generating schemas for operation create_user Connector Build Finished Connector Deploy Started Connector Deploy Request Sent Deployment UUID: 3c5e4a2d-ad3a-58e9-8e2d-eff986f62387 ``` **Use when:** Your connector is tested and ready for the Tray platform. **Important:** Deploy separately per region using the appropriate `TRAY_API_BASE_URL` and token. --- #### `tray-cdk deployment get CONNECTOR_NAME CONNECTOR_VERSION UUID` Polls the status of a deployment. ```bash tray-cdk deployment get my-company-my-service 1.0 3c5e4a2d-ad3a-58e9-8e2d-eff986f62387 ``` **Status progression:** `building` → `deploying` → `deployed` (or `failed`) **Use when:** After `deployment create`, to check if deployment succeeded. May need to poll multiple times. --- ### 3.5 Permission Commands #### `tray-cdk permissions add [CONNECTOR_NAME] [CONNECTOR_VERSION]` Shares a deployed connector with users in your organization. ```bash tray-cdk permissions add my-connector 1.0 --email="alice@company.com, bob@company.com" ``` **Critical context:** After deploying with an RBAC token, the connector is owned by that API user. Nobody (including you) can see it in the Tray UI until you explicitly share it. **Use when:** Immediately after successful deployment. --- #### `tray-cdk permissions list [CONNECTOR_NAME] [CONNECTOR_VERSION]` Lists who has access to a connector version. ```bash tray-cdk permissions list my-connector 1.0 ``` **Use when:** Auditing access, or checking if sharing was successful. --- ## 4. End-to-End Development Flow This is the canonical order of operations from zero to deployed connector. ### Phase 1: One-Time Organization Setup ```bash # 1. Install CLI npm install -g @trayio/cdk-cli # 2. Verify tray-cdk version # 3. Set API token export TRAY_API_TOKEN= # 4. Create namespace (once per org, ever) tray-cdk namespace create my-company --us ``` ### Phase 2: Project Initialization ```bash # 5. Scaffold project tray-cdk connector init my-service -i cd my-service # 6. Delete sample operation rm -rf src/get_post # 7. If the service requires auth: create a Custom Service in the Tray UI # (Settings → Services → New Service) # Copy the unique service name (e.g., L3DJ7C5mqVj1yG_my-service) ``` ### Phase 3: Configure Connector Foundation Edit these files in order: ``` connector.json → Set name, version, title, service reference src/Auth.ts → Define authentication type src/GlobalConfig.ts → Set base URL and auth headers src/test.ctx.json → Add real tokens for local testing (NEVER commit) ``` ### Phase 4: Build Operations (repeat per operation) ```bash # 8. Add operation tray-cdk connector add-operation get_users http # 9. Edit the 4 operation files: # src/get_users/input.ts → Define input parameters # src/get_users/output.ts → Define response structure # src/get_users/handler.ts → Implement operation logic # src/get_users/handler.test.ts → Write test cases # 10. Test tray-cdk connector test get_users ``` ### Phase 5: Deploy ```bash # 11. Set deployment env vars export TRAY_API_TOKEN= export TRAY_API_BASE_URL=https://api.tray.io # 12. Deploy (runs tests automatically) tray-cdk deployment create # 13. Check status tray-cdk deployment get -my-service 1.0 # 14. Share with users tray-cdk permissions add -my-service 1.0 -e="me@company.com, team@company.com" ``` ### Phase 6: Iterate ```bash # Add more operations → repeat Phase 4 # Bump version in connector.json # Re-deploy → repeat Phase 5 ``` **Note:** Connector versions do NOT auto-update in existing Tray workflows. After publishing a new version, users must manually update their workflows. --- ## 5. Project Structure Deep Dive ``` my-connector/ ├── connector.json # Connector metadata & config ├── package.json # npm dependencies (cdk-dsl, cdk-runtime) ├── package-lock.json ├── jest.config.js # Test config (do not modify) ├── tsconfig.json # TypeScript config (do not modify) ├── node_modules/ └── src/ ├── Auth.ts # Auth type definition (shared by all operations) ├── GlobalConfig.ts # Shared HTTP config (base URL, headers, auth) ├── test.ctx.json # Auth context for tests (NEVER commit to repo) ├── get_users/ │ ├── operation.json # {"name":"get_users","title":"Get Users","description":"..."} │ ├── input.ts # TypeScript type for input │ ├── output.ts # TypeScript type for output │ ├── handler.ts # Operation logic │ └── handler.test.ts # Test cases ├── create_user/ │ ├── operation.json │ ├── input.ts │ ├── output.ts │ ├── handler.ts │ └── handler.test.ts └── list_users_ddl/ # DDL operation ├── operation.json # {"name":"list_users_ddl","title":"...","type":"ddl"} ├── input.ts ├── output.ts ├── handler.ts └── handler.test.ts ``` ### connector.json ```json { "name": "[namespace]-my-service", "version": "1.0", "title": "My Service", "description": "Connector for My Service API", "service": { "name": "[unique-service-name-from-tray-ui]", "version": "1" }, "tags": ["service"], "isTrigger": false, "rawHttp": { "enabled": true } } ``` Key fields: - `name` — Must be prefixed with your org namespace - `version` — Semver-like. Increment for new deployments - `service.name` — The unique service name from the Tray UI - `rawHttp.enabled` — When `true`, users can call any endpoint via the Builder UI without a dedicated operation ### operation.json For regular operations: ```json { "name": "get_users", "title": "Get Users", "description": "Retrieves a list of users from the API" } ``` For DDL operations (hidden from operation list): ```json { "name": "list_users_ddl", "title": "List Users DDL", "description": "Returns user list for dropdown", "type": "ddl" } ``` --- ## 6. Authentication Configuration ### src/Auth.ts Defines the shape of authentication tokens. All auth handling (OAuth flow, token refresh) is managed by Tray's service layer — you only define the type. #### Token Auth (most common) For APIs that use Bearer tokens, API keys, or JWTs: ```typescript import { TokenOperationHandlerAuth } from "@trayio/cdk-dsl/connector/operation/OperationHandler"; export type UserAuth = { access_token: string; // Property name is flexible }; export type AppAuth = {}; // Empty for token auth export type MyServiceAuth = TokenOperationHandlerAuth; ``` #### OAuth2 Authorization Code For standard OAuth2 services: ```typescript import { Oauth2OperationHandlerAuth } from "@trayio/cdk-dsl/connector/operation/OperationHandler"; export type UserAuth = { access_token: string; }; export type AppAuth = { client_id: string; client_secret: string; auth_url: string; token_url: string; scopes: string; }; export type MyServiceAuth = Oauth2OperationHandlerAuth; ``` #### OAuth2 Client Credentials For machine-to-machine flows: ```typescript import { Oauth2ClientCredentialsOperationHandlerAuth } from "@trayio/cdk-dsl/connector/operation/OperationHandler"; // Same pattern, use this type instead ``` #### OAuth2 Password Grant ```typescript import { Oauth2PasswordOperationHandlerAuth } from "@trayio/cdk-dsl/connector/operation/OperationHandler"; ``` #### OAuth1 Rare, for legacy services: ```typescript import { Oauth1OperationHandlerAuth } from "@trayio/cdk-dsl/connector/operation/OperationHandler"; ``` #### No Auth (helper/utility connector) ```typescript // For connectors that don't need user authentication export type MyHelperAuth = any; ``` ### src/test.ctx.json Provides real auth tokens for local testing. **NEVER commit this file.** ```json { "auth": { "user": { "access_token": "your-real-api-token-for-testing" }, "app": {} } } ``` --- ## 7. Global Configuration ### src/GlobalConfig.ts Defines shared HTTP configuration applied to all operations. Introduced in CDK v4. ```typescript import { OperationGlobalConfigHttp } from "@trayio/cdk-dsl/connector/operation/OperationGlobalConfig"; import { MyServiceAuth } from "./Auth"; export const globalConfigHttp = OperationGlobalConfigHttp.create() .withBaseUrl((_ctx) => `https://api.example.com`) .withBearerToken((ctx) => ctx.auth!.user.access_token); ``` **Available methods:** | Method | Purpose | |---|---| | `.withBaseUrl((ctx) => string)` | Set base URL for all operations | | `.withBearerToken((ctx) => string)` | Add `Authorization: Bearer ` header | | `.addHeader(name, (ctx) => value)` | Add a custom header | **Rules:** - Do NOT rename the `globalConfigHttp` variable (used by Raw HTTP operation internally) - Do NOT delete this file - Global config only works with HTTP handlers, NOT composite handlers - Individual operations can skip or override global config **Using in handlers:** ```typescript handler.withGlobalConfiguration(globalConfigHttp).usingHttp(...) ``` **Skipping global config (handler manages its own auth/URL):** ```typescript handler.usingHttp((http) => http.get("https://full-url.com/endpoint") .handleRequest((ctx, input, request) => request.withBearerToken(ctx.auth!.user.access_token).withoutBody() ) .handleResponse(...) ) ``` --- ## 8. Building HTTP Operations HTTP operations make exactly one HTTP call. This is the most common operation type. ### Complete Handler Pattern ```typescript import { OperationHandlerSetup } from "@trayio/cdk-dsl/connector/operation/OperationHandlerSetup"; import { MyServiceAuth } from "../Auth"; import { GetUsersInput } from "./input"; import { GetUsersOutput } from "./output"; import { globalConfigHttp } from "../GlobalConfig"; export const getUsersHandler = OperationHandlerSetup.configureHandler< MyServiceAuth, GetUsersInput, GetUsersOutput >((handler) => handler .addInputValidation((validation) => // OPTIONAL validation .condition((ctx, input) => input.page_size <= 100) .errorMessage(() => "Maximum page size is 100") ) .withGlobalConfiguration(globalConfigHttp) // OPTIONAL .usingHttp((http) => http .get("/users") // HTTP method + path .handleRequest((ctx, input, request) => // Build request request .addQueryString("page", input.page.toString()) .addQueryString("limit", input.page_size.toString()) .withoutBody() // REQUIRED terminator ) .handleResponse((ctx, input, response) => // Parse response response.parseWithBodyAsJson() ) ) ); ``` ### handleRequest — Building the Request The request object provides chainable methods. **Every request MUST end with a body method.** #### Path parameters ```typescript // URL: /users/:userId/posts/:postId .handleRequest((ctx, input, request) => request .addPathParameter("userId", input.userId) .addPathParameter("postId", input.postId) .withoutBody() ) ``` #### Query strings ```typescript .handleRequest((ctx, input, request) => request .addQueryString("status", input.status) .addQueryString("limit", input.limit.toString()) .withoutBody() ) ``` #### Headers ```typescript .handleRequest((ctx, input, request) => request .addHeader("X-Custom-Header", "value") .addHeader("Accept", "application/json") .withoutBody() ) ``` #### Bearer token (when NOT using global config) ```typescript .handleRequest((ctx, input, request) => request .withBearerToken(ctx.auth!.user.access_token) .withoutBody() ) ``` #### JSON body (POST/PUT/PATCH) ```typescript // Send entire input as body .handleRequest((ctx, input, request) => request.withBodyAsJson(input) ) // Send partial input as body .handleRequest((ctx, input, request) => request.withBodyAsJson({ name: input.name, email: input.email }) ) ``` #### Form URL-encoded body ```typescript .handleRequest((ctx, input, request) => request.withBodyAsFormUrlEncoded({ grant_type: "client_credentials", scope: input.scope }) ) ``` #### Plain text body ```typescript .handleRequest((ctx, input, request) => request.withBodyAsText(input.xmlPayload) ) ``` #### Multipart form-data (file + fields) ```typescript .handleRequest((ctx, input, request) => request.withBodyAsMultipart({ fields: { description: "My file" }, files: { file: input.file } }) ) ``` #### Binary file upload ```typescript .handleRequest((ctx, input, request) => request.withBodyAsFile(input.file) ) ``` #### No body (GET, DELETE) ```typescript .handleRequest((ctx, input, request) => request.withoutBody() ) ``` ### handleResponse — Parsing the Response #### JSON response (most common) ```typescript .handleResponse((ctx, input, response) => response.parseWithBodyAsJson() ) ``` #### Transform response before returning ```typescript .handleResponse((ctx, input, response) => { const body = response.parseWithBodyAsJson<{ data: any[] }>(); if (body.isSuccess) { return OperationHandlerResult.success({ items: body.value.data, count: body.value.data.length }); } return body; }) ``` #### Text response ```typescript .handleResponse((ctx, input, response) => response.parseWithBodyAsText((text) => OperationHandlerResult.success({ content: text }) ) ) ``` #### File download ```typescript .handleResponse((ctx, input, response) => response.parseWithBodyAsFile((file) => OperationHandlerResult.success({ file }) ) ) ``` ### HTTP Error Handling #### Custom error messages ```typescript .handleResponse((ctx, input, response) => response .withErrorHandling(() => { const status = response.getStatusCode(); if (status === 404) { return OperationHandlerResult.failure( OperationHandlerError.userInputError("Resource not found") ); } return OperationHandlerResult.failure( OperationHandlerError.apiError(`API error: ${status}`) ); }) .parseWithBodyAsJson() ) ``` #### Parse 3rd party error bodies ```typescript .handleResponse((ctx, input, response) => response .withJsonErrorHandling<{ message: string }>((body) => OperationHandlerResult.failure( OperationHandlerError.apiError( `API error: ${body.message} (${response.getStatusCode()})`, { statusCode: response.getStatusCode() } ) ) ) .parseWithBodyAsJson() ) ``` Both `withErrorHandling()` and `withJsonErrorHandling()` are automatically invoked only on non-2xx responses. ### HTTP Method Examples ```typescript // GET http.get("/users") // POST http.post("/users") // PUT http.put("/users/:userId") // PATCH http.patch("/users/:userId") // DELETE http.delete("/users/:userId") ``` --- ## 9. Building Composite Operations Composite operations handle anything that isn't a single HTTP call. ### Basic Pattern ```typescript import { OperationHandlerSetup } from "@trayio/cdk-dsl/connector/operation/OperationHandlerSetup"; import { OperationHandlerResult, OperationHandlerError } from "@trayio/cdk-dsl/connector/operation/OperationHandler"; import { MyAuth } from "../Auth"; import { MyInput } from "./input"; import { MyOutput } from "./output"; export const myHandler = OperationHandlerSetup.configureHandler< MyAuth, MyInput, MyOutput >((handler) => handler.usingComposite(async (ctx, input, invoke) => { try { // Your logic here return OperationHandlerResult.success({ /* output */ }); } catch (error) { return OperationHandlerResult.failure( OperationHandlerError.connectorError("Something went wrong") ); } }) ); ``` ### Utility / Helper (npm packages, no API calls) ```typescript import TurndownService from "turndown"; export const htmlToMarkdownHandler = OperationHandlerSetup.configureHandler< MyAuth, HtmlToMarkdownInput, HtmlToMarkdownOutput >((handler) => handler.usingComposite(async (ctx, { htmlString }, invoke) => { const turndownService = new TurndownService(); const markdownString = turndownService.turndown(htmlString); return OperationHandlerResult.success({ markdownString }); }) ); ``` ### Invoking Other Operations ```typescript import { getProductsHandler } from "../get_products/handler"; export const getProductsDdlHandler = OperationHandlerSetup.configureHandler< MyAuth, Input, Output >((handler) => handler.usingComposite(async (ctx, input, invoke) => { // invoke(handlerReference)(input) → Promise> const result = await invoke(getProductsHandler)({ storeId: input.storeId }); if (result.isFailure) { return result; // Propagate failure } // result.value is now typed as GetProductsOutput const ddlItems = result.value.products.map((p) => ({ text: p.name, value: p.id })); return OperationHandlerResult.success({ result: ddlItems }); }) ); ``` ### Multiple Concurrent API Calls ```typescript import axios from "axios"; export const generateReportHandler = OperationHandlerSetup.configureHandler< MyAuth, Input, Output >((handler) => handler.usingComposite(async (ctx, input, invoke) => { try { const headers = { Authorization: `Bearer ${ctx.auth?.user.access_token}`, "Content-Type": "application/json" }; const [users, orders, products] = await Promise.all([ axios.get("https://api.example.com/users", { headers }), axios.get("https://api.example.com/orders", { headers }), axios.get("https://api.example.com/products", { headers }) ]); return OperationHandlerResult.success({ userCount: users.data.length, orderCount: orders.data.length, productCount: products.data.length }); } catch (error) { return OperationHandlerResult.failure( OperationHandlerError.connectorError("Failed to generate report") ); } }) ); ``` ### Composite Error Types | Error Type | Auto-Retried? | Visible in Tray Logs? | When to Use | |---|---|---|---| | `OperationHandlerError.connectorError(msg)` | Yes | No | Server errors, library crashes | | `OperationHandlerError.apiError(msg, meta?)` | No | No | General API errors | | `OperationHandlerError.userInputError(msg)` | No | Yes | Bad user input | | `OperationHandlerError.oauthRefreshError(msg)` | Yes | Yes | 401/403 (triggers token refresh) | | `OperationHandlerError.timeoutError(msg)` | No | Yes | Manual timeout | | `OperationHandlerError.skipTriggerError(msg)` | No | No | Trigger filtering only | **Important:** Composite operations have a **120-second timeout**. Global config does NOT work with composite operations. --- ## 10. Dynamic Dropdown Lists (DDLs) DDLs populate dropdown fields in the Tray Builder UI with data fetched from an API. ### Step 1: Create the DDL Operation ```bash tray-cdk connector add-operation list_genres_ddl composite ``` ### Step 2: Mark as DDL in operation.json ```json { "name": "list_genres_ddl", "title": "List Genres DDL", "description": "Returns genres for dropdown selection", "type": "ddl" } ``` The `"type": "ddl"` hides this operation from the user's visible operation list. ### Step 3: Define Output Type ```typescript // output.ts import { DDLOperationOutput } from "@trayio/cdk-dsl/connector/operation/OperationHandler"; // Use if values are numeric IDs, if string IDs export type ListGenresDdlOutput = DDLOperationOutput; ``` ### Step 4: Implement Handler ```typescript // handler.ts import axios from "axios"; export const listGenresDdlHandler = OperationHandlerSetup.configureHandler< MyAuth, ListGenresDdlInput, ListGenresDdlOutput >((handler) => handler.usingComposite(async (ctx, input, invoke) => { const response = await axios.get("https://api.example.com/genres", { headers: { Authorization: `Bearer ${ctx.auth?.user.access_token}` } }); if (response.status !== 200) { return OperationHandlerResult.failure( OperationHandlerError.connectorError("Failed to fetch genres") ); } const ddlItems = response.data.map((genre: any) => ({ text: genre.name, // Display label in dropdown value: genre.id // Actual value passed to the operation })); return OperationHandlerResult.success({ result: ddlItems }); }) ); ``` ### Step 5: Use DDL in Another Operation's Input ```typescript // src/get_movies_by_genre/input.ts export type GetMoviesByGenreInput = { /** * @title Genre * @lookupOperation list_genres_ddl * @lookupAuthRequired true */ genre: number; }; ``` If the DDL needs parameters from other input fields, use triple braces: ```typescript export type SendMessageInput = { workspaceId: string; /** * @title User * @lookupOperation list_users_ddl * @lookupInput {"workspaceId": "{{{workspaceId}}}"} * @lookupAuthRequired true */ userId: string; }; ``` --- ## 11. Input Configuration & UI Annotations ### Data Types ```typescript export type ExampleInput = { name: string; // Text field age: number; // Number field /** @TJS-type integer */ count: number; // Integer-only field isActive: boolean; // Checkbox address: { // Nested object city: string; zip: string; }; tags: string[]; // Array }; ``` ### Required vs Optional ```typescript export type ExampleInput = { requiredField: string; // Mandatory (red * in UI) optionalField?: string; // Optional (no red *) }; ``` ### Enums (Static Dropdowns) ```typescript enum Status { active = 'active', // Auto-displays as "Active" inactive = 'inactive', // Auto-displays as "Inactive" } export type ExampleInput = { status: Status; }; ``` With custom labels: ```typescript /** * @enumLabels Currently Active, Not Active, Permanently Removed */ enum Status { active = 'active', inactive = 'inactive', deleted = 'deleted', } ``` ### Field Formatting ```typescript export type ExampleInput = { /** @format datetime */ timestamp: string; // Date-time picker /** @format code */ htmlContent: string; // Code editor modal /** @format text */ description: string; // Expandable text area }; ``` ### Default Values ```typescript export type ExampleInput = { /** @default 10 */ pageSize: number; /** @default true */ includeDeleted: boolean; /** @default "active" */ status: string; }; ``` ### Advanced Fields (Hidden by Default) ```typescript export type ExampleInput = { query: string; // Always visible /** @advanced true */ timeout?: number; // Hidden under "Advanced" section }; ``` ### Union Types (Multiple Input Shapes) ```typescript type ImageOrText = Image | Text; /** @title Image */ type Image = { name: string; src: string }; /** @title Text */ type Text = { text: string }; export type ExampleInput = { elements: ImageOrText[]; // User picks type from dropdown per item }; ``` ### Reusing Types Across Operations ```typescript // shared/Pagination.ts export type PaginationFields = { page?: number; pageSize?: number; }; // src/get_users/input.ts import { PaginationFields } from "../shared/Pagination"; type GetUsersSpecificFields = { status: string; }; export type GetUsersInput = GetUsersSpecificFields & PaginationFields; ``` --- ## 12. Output Configuration Defines the response shape. Used by the Tray Builder for JSONpath navigation. ### Pass-through (return API response as-is) ```typescript export type GetUsersOutput = { page: number; results: User[]; total_pages: number; total_results: number; }; type User = { id: number; name: string; email: string; }; ``` ### Transformed output (reshape in handler) If your handler transforms the response: ```typescript // handler.ts transforms the response .handleResponse((ctx, input, response) => { const raw = response.parseWithBodyAsJson(); if (raw.isSuccess) { return OperationHandlerResult.success({ userNames: raw.value.data.map(u => u.name), count: raw.value.data.length }); } return raw; }) // output.ts matches the transformed shape export type GetUsersOutput = { userNames: string[]; count: number; }; ``` --- ## 13. Testing Operations ### Test Structure (BDD-style DSL) ```typescript import { OperationHandlerTestSetup } from "@trayio/cdk-dsl/connector/operation/OperationHandlerTest"; import { OperationHandlerResult } from "@trayio/cdk-dsl/connector/operation/OperationHandler"; import { getUsersHandler } from "./handler"; import "@trayio/cdk-runtime/connector/operation/OperationHandlerTestRunner"; OperationHandlerTestSetup.configureHandlerTest( getUsersHandler, (handlerTest) => handlerTest .usingHandlerContext("test") // Loads test.ctx.json .nothingBeforeAll() // No shared setup .testCase("should return users", (testCase) => testCase .givenNothing() // No per-test setup .when(() => ({ // Provide input page: 1, page_size: 10 })) .then(({ output }) => { // Assert output const value = OperationHandlerResult.getSuccessfulValueOrFail(output); expect(value.results.length).toBeGreaterThan(0); expect(value.page).toEqual(1); }) .finallyDoNothing() // No per-test cleanup ) .nothingAfterAll() // No shared cleanup ); ``` ### Test with Setup/Teardown ```typescript OperationHandlerTestSetup.configureHandlerTest( updateUserHandler, (handlerTest) => handlerTest .usingHandlerContext("test") .beforeAll<{ orgId: string }>(async (auth, invoke) => { // Create test org for all tests const result = await invoke(createOrgHandler)({ name: "test-org" }); return OperationHandlerResult.map(result, (output) => ({ orgId: output.id })); }) .testCase("should update user", (testCase) => testCase .given<{ userId: string }>(async (auth, testContext, invoke) => { // Create test user for this test const result = await invoke(createUserHandler)({ orgId: testContext.orgId, name: "Test User" }); return OperationHandlerResult.map(result, (output) => ({ userId: output.id })); }) .when((auth, testContext, testCaseContext) => ({ userId: testCaseContext.userId, name: "Updated Name" })) .then(({ output }) => { const value = OperationHandlerResult.getSuccessfulValueOrFail(output); expect(value.name).toEqual("Updated Name"); }) .finally(async ({ testCaseContext, invoke }) => { // Clean up test user await invoke(deleteUserHandler)({ userId: testCaseContext.userId }); }) ) .afterAll(async ({ testContext, invoke }) => { // Clean up test org await invoke(deleteOrgHandler)({ orgId: testContext.orgId }); }) ); ``` ### Running Tests ```bash # All operations tray-cdk connector test # Specific operation tray-cdk connector test get_users # Verbose (shows input/output) tray-cdk connector test get_users -v # Via npm npm test ``` --- ## 14. File Handling The CDK uses a `FileReference` type for files: ```typescript import { FileReference } from "@trayio/cdk-dsl/dist/connector/operation/OperationHandler"; type FileReference = { name: string; url: string; mime_type: string; expires: number; }; ``` ### Download ```typescript // output.ts export type DownloadOutput = { file: FileReference }; // handler.ts handler.usingHttp((http) => http.get("/files/:fileId") .handleRequest((ctx, input, request) => request.addPathParameter("fileId", input.fileId).withoutBody() ) .handleResponse((ctx, input, response) => response.parseWithBodyAsFile((file) => OperationHandlerResult.success({ file }) ) ) ); ``` ### Upload (Binary) ```typescript // input.ts export type UploadInput = { file: FileReference }; // handler.ts handler.usingHttp((http) => http.post("/upload") .handleRequest((ctx, input, request) => request.withBodyAsFile(input.file)) .handleResponse((ctx, input, response) => response.parseWithBodyAsJson()) ); ``` ### Upload (Multipart) ```typescript // input.ts export type UploadInput = { file: FileReference; description: string }; // handler.ts handler.usingHttp((http) => http.post("/upload") .handleRequest((ctx, input, request) => request.withBodyAsMultipart({ fields: { description: input.description }, files: { file: input.file } }) ) .handleResponse((ctx, input, response) => response.parseWithBodyAsJson()) ); ``` --- ## 15. Deployment ### Via CLI (Standard) ```bash export TRAY_API_TOKEN= export TRAY_API_BASE_URL=https://api.tray.io # US region tray-cdk deployment create # Returns UUID for status polling tray-cdk deployment get # Status: building → deploying → deployed | failed ``` ### Via API (for CI/CD) ```bash # 1. Build tray-cdk connector build # 2. Base64 encode the zip ENCODED=$(base64 -i .tray/connector.zip) # 3. POST to deployment endpoint curl -X POST \ "https://api.tray.io/cdk/v1/deployments/connectors/${CONNECTOR_NAME}/versions/${VERSION}/deploy-connector-from-source" \ -H "Authorization: Bearer ${TRAY_API_TOKEN}" \ -H "Content-Type: application/json" \ -d "{\"connectorSourceCode\": \"${ENCODED}\"}" # 4. Poll status curl "https://api.tray.io/cdk/v1/deployments/connectors/${CONNECTOR_NAME}/versions/${VERSION}/${DEPLOYMENT_ID}" \ -H "Authorization: Bearer ${TRAY_API_TOKEN}" # 5. Share curl -X POST \ "https://api.tray.io/cdk/v1/permissions/connectors/${CONNECTOR_NAME}/versions/${VERSION}/share-with-emails" \ -H "Authorization: Bearer ${TRAY_API_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"emails": ["user@company.com"]}' ``` ### Regional Deployment | Region | `TRAY_API_BASE_URL` | |---|---| | US | `https://api.tray.io` | | EU | `https://api.eu1.tray.io` | | APAC | `https://api.ap1.tray.io` | Deploy separately per region with corresponding token and URL. --- ## 16. Permissions & Sharing After deployment, the connector is owned by the API token user. **Nobody can see it until you share.** ```bash # Share with specific users tray-cdk permissions add my-connector 1.0 --email="alice@co.com, bob@co.com" # List who has access tray-cdk permissions list my-connector 1.0 ``` Connectors cannot be shared across different Tray organizations. --- ## 17. Typical Flows ### Flow A: REST API Connector with Token Auth This is the most common flow: building a connector for a REST API that uses API key or Bearer token authentication. ``` 1. tray-cdk connector init acme-crm -i 2. cd acme-crm 3. rm -rf src/get_post 4. Create Custom Service in Tray UI (token type) 5. Edit connector.json → add service name, title 6. Edit Auth.ts → TokenOperationHandlerAuth 7. Edit GlobalConfig.ts → withBaseUrl + withBearerToken 8. Edit test.ctx.json → add real API token 9. tray-cdk connector add-operation list_contacts http 10. Edit input/output/handler/handler.test 11. tray-cdk connector test list_contacts 12. tray-cdk connector add-operation get_contact http 13. Edit input/output/handler/handler.test 14. tray-cdk connector test get_contact 15. tray-cdk connector add-operation create_contact http 16. ...repeat for each endpoint... 17. tray-cdk deployment create 18. tray-cdk permissions add 1.0 -e="me@co.com" ``` ### Flow B: OAuth2 Service Connector Same as Flow A, but with OAuth2 setup: ``` 1. Create OAuth app with the third-party service 2. Get client_id, client_secret, auth_url, token_url 3. Create Custom Service in Tray UI (OAuth2 type) - Set redirect URI: https://auth.tray.io/oauth2/token - Enter client_id, client_secret 4. tray-cdk connector init my-oauth-service -i 5. Auth.ts → Oauth2OperationHandlerAuth 6. GlobalConfig.ts → withBaseUrl + withBearerToken 7. test.ctx.json → add a manually obtained access_token for testing 8. ...build operations as normal... ``` ### Flow C: Utility/Helper Connector (No Auth, No API) For data transformation connectors using npm packages: ``` 1. tray-cdk connector init data-helpers -i 2. cd data-helpers 3. rm -rf src/get_post 4. Auth.ts → set type to `any` 5. No Custom Service needed 6. connector.json → remove "service" block, set "tags": ["helper"] 7. npm install xml-js turndown marked (whatever packages you need) 8. tray-cdk connector add-operation json_to_xml composite 9. tray-cdk connector add-operation html_to_markdown composite 10. tray-cdk connector add-operation markdown_to_html composite 11. ...implement each using npm packages... 12. Deploy ``` ### Flow D: Connector with DDL Dropdowns Adding dynamic dropdowns to an existing service connector: ``` 1. Build base connector (Flow A or B) 2. tray-cdk connector add-operation list_categories_ddl composite 3. operation.json → add "type": "ddl" 4. output.ts → DDLOperationOutput 5. handler.ts → fetch categories, return {text, value} pairs 6. handler.test.ts → verify DDL returns expected items 7. tray-cdk connector test list_categories_ddl 8. tray-cdk connector add-operation get_items_by_category http 9. input.ts → add @lookupOperation list_categories_ddl annotation 10. ...implement handler... 11. Deploy ``` ### Flow E: OpenAPI Bootstrap Fast-track connector creation from an API spec: ``` 1. Obtain the OpenAPI 3.0 JSON spec from the service 2. tray-cdk connector import ./api-spec.json my-service 3. cd my-service && npm install 4. Review errors.json if generated 5. Configure Auth.ts and GlobalConfig.ts 6. Edit test.ctx.json with real tokens 7. Review/fix auto-generated operations 8. Write tests for each operation 9. tray-cdk connector test 10. Deploy ``` ### Flow F: Adding Operations to Existing Connector Extending a previously deployed connector: ``` 1. cd existing-connector 2. tray-cdk connector add-operation new_operation http 3. Implement the operation files 4. tray-cdk connector test new_operation 5. Bump version in connector.json (e.g., "1.0" → "1.1") 6. tray-cdk deployment create 7. tray-cdk permissions add 1.1 -e="team@co.com" 8. NOTE: Existing workflows using v1.0 must be manually updated to v1.1 ``` ### Flow G: Multi-Region Deployment Deploying the same connector to US, EU, and APAC: ``` # US export TRAY_API_TOKEN= export TRAY_API_BASE_URL=https://api.tray.io tray-cdk deployment create tray-cdk permissions add 1.0 -e="us-team@co.com" # EU export TRAY_API_TOKEN= export TRAY_API_BASE_URL=https://api.eu1.tray.io tray-cdk deployment create tray-cdk permissions add 1.0 -e="eu-team@co.com" # APAC export TRAY_API_TOKEN= export TRAY_API_BASE_URL=https://api.ap1.tray.io tray-cdk deployment create tray-cdk permissions add 1.0 -e="ap-team@co.com" ``` --- ## 18. Troubleshooting ### Installation & Setup Issues #### CLI not found after install ``` Error: tray-cdk: command not found ``` **Cause:** npm global bin directory not in PATH. **Fix:** ```bash # Find where npm installs global packages npm config get prefix # Add /bin to your PATH export PATH="$(npm config get prefix)/bin:$PATH" # Or reinstall with explicit path npm install -g @trayio/cdk-cli ``` --- #### Node version too old ``` Error: The engine "node" is incompatible with this module ``` **Fix:** Upgrade to Node.js v18 or higher: ```bash nvm install 18 nvm use 18 ``` --- ### Namespace Issues #### "Namespace already exists" **Cause:** Each org gets exactly one namespace. **Fix:** Use `tray-cdk namespace get ` to retrieve the existing one. --- #### "Invalid API token" during namespace operations **Fix:** Verify `TRAY_API_TOKEN` is set and valid: ```bash echo $TRAY_API_TOKEN # Should show your token # Re-export if needed export TRAY_API_TOKEN= ``` --- ### Build & Compilation Issues #### TypeScript compilation errors in handler.ts **Common cause:** Mismatched types between input/output definitions and handler usage. **Fix:** Ensure the generic types match: ```typescript // These three types must all align: OperationHandlerSetup.configureHandler< MyAuth, // Must match Auth.ts export MyInput, // Must match input.ts export MyOutput // Must match output.ts export > ``` --- #### "Request must end with a body method" **Cause:** `handleRequest` doesn't terminate with a body function. **Fix:** Always end the request chain with one of: - `.withoutBody()` — for GET, DELETE - `.withBodyAsJson(body)` — for POST, PUT, PATCH with JSON - `.withBodyAsText(body)` — for plain text - `.withBodyAsFormUrlEncoded(body)` — for form data - `.withBodyAsMultipart({fields, files})` — for file uploads - `.withBodyAsFile(fileRef)` — for binary uploads --- #### "Cannot find module '@trayio/cdk-dsl/...'" **Fix:** ```bash rm -rf node_modules package-lock.json npm install ``` --- ### Test Issues #### Jest timeout (test takes too long) ``` Error: Timeout - Async callback was not invoked within the 5000 ms timeout ``` **Cause:** Default Jest timeout is 5 seconds. Real API calls often exceed this. **Fix (per test file):** Add at the top of `handler.test.ts`: ```typescript jest.setTimeout(30000); // 30 seconds ``` **Fix (all tests):** Edit `jest.config.js`: ```json { "testTimeout": 30000 } ``` --- #### Test fails with auth error **Cause:** Invalid or expired token in `test.ctx.json`. **Fix:** Update `test.ctx.json` with a fresh, valid token: ```json { "auth": { "user": { "access_token": "" }, "app": {} } } ``` --- #### "Cannot read property 'auth' of undefined" **Cause:** `test.ctx.json` is empty or malformed. **Fix:** Ensure proper JSON structure with auth.user and auth.app fields. --- #### Debugging test failures Add logging to your handler: ```typescript .handleRequest((ctx, input, request) => { console.log("REQUEST:", JSON.stringify(request.withoutBody(), null, 2)); return request.withoutBody(); }) .handleResponse((ctx, input, response) => { console.log("STATUS:", response.getStatusCode()); console.log("RESPONSE:", JSON.stringify(response, null, 2)); return response.parseWithBodyAsJson(); }) ``` Or use verbose mode: ```bash tray-cdk connector test my_operation -v ``` --- ### Deployment Issues #### "ENOTEMPTY: directory not empty, rmdir './tmp/connectors/.../node_modules/@types/node'" ``` Error (500): {"message":"ENOTEMPTY: directory not empty..."} ``` **Fix:** ```bash rm -rf node_modules npm install tray-cdk deployment create ``` --- #### Deployment stuck at "building" **Cause:** Large connector or server-side delay. **Fix:** Wait and keep polling: ```bash tray-cdk deployment get ``` If it stays in "building" for more than 10 minutes, try re-deploying. --- #### "Tests failed" during deployment **Cause:** `deployment create` runs tests first. If any test fails, deployment is aborted. **Fix:** Run tests locally first to identify and fix the failure: ```bash tray-cdk connector test -v ``` --- #### Deployed but connector not visible in Tray UI **Cause:** Connector is owned by the API user token. You haven't shared it. **Fix:** ```bash tray-cdk permissions add -e="your-email@company.com" ``` --- #### Wrong region **Cause:** Deployed to US but trying to use in EU. **Fix:** Deploy to each region separately: ```bash export TRAY_API_BASE_URL=https://api.eu1.tray.io tray-cdk deployment create ``` --- ### Handler Issues #### Global config not applied **Cause:** Missing `.withGlobalConfiguration(globalConfigHttp)` in the handler chain. **Fix:** Add it before `.usingHttp()`: ```typescript handler.withGlobalConfiguration(globalConfigHttp).usingHttp(...) ``` --- #### Global config with composite handler **Cause:** Global config ONLY works with HTTP handlers, not composite. **Fix:** Access auth directly from `ctx` in composite handlers: ```typescript handler.usingComposite(async (ctx, input, invoke) => { const token = ctx.auth?.user.access_token; // Use token directly with axios or other HTTP library }); ``` --- #### "ctx.auth is possibly null" **Cause:** TypeScript strict null checking. **Fix:** Use the non-null assertion operator: ```typescript ctx.auth!.user.access_token ``` --- #### Operation times out on Tray UI **Cause:** CDK connector operations have a 120-second timeout. **Fix:** Optimize the operation. If making multiple API calls, use `Promise.all()` for concurrency. Consider breaking into multiple smaller operations. --- ### DDL Issues #### DDL not appearing in dropdown **Cause 1:** Missing `"type": "ddl"` in operation.json. **Fix:** Add `"type": "ddl"` to the DDL operation's operation.json. **Cause 2:** Missing `@lookupOperation` annotation in the consuming operation's input.ts. **Fix:** Add the annotation: ```typescript /** @lookupOperation list_items_ddl */ myField: string; ``` --- #### DDL returns empty dropdown **Cause:** DDL handler returns empty array or fails silently. **Fix:** Test the DDL operation directly: ```bash tray-cdk connector test list_items_ddl -v ``` Ensure it returns `{ result: [{ text: "...", value: "..." }, ...] }`. --- ### Version & Migration Issues #### Upgrading from CDK v3 to v4 ```bash # 1. Update CLI npm update -g @trayio/cdk-cli # 2. Verify version tray-cdk version # Must be 4.0.0+ # 3. Build (creates GlobalConfig.ts automatically) npm run build # 4. Update package.json dependencies # Set @trayio/cdk-dsl and @trayio/cdk-runtime to latest v4+ versions # 5. Clean install rm -rf node_modules package-lock.json npm install # 6. Configure GlobalConfig.ts with base URL and auth # 7. If no-auth connector: change auth type from `never` to `any` # 8. Re-deploy ``` --- #### Workflows not using new connector version **Cause:** Connector versions do NOT auto-update in existing workflows. **Fix:** Users must manually update each workflow to use the new connector version in the Tray Builder UI. Warn users that updating may break existing property panel configurations. --- ### Common Gotchas Summary | Gotcha | Resolution | |---|---| | Forgot to delete sample `get_post` operation | Delete `src/get_post/` after init | | `test.ctx.json` committed to repo | Add to `.gitignore` immediately | | Missing `withoutBody()` on GET requests | Always terminate request chain with body method | | Using global config in composite handler | Use `ctx.auth` directly instead | | DDL operation visible in operation list | Add `"type": "ddl"` to operation.json | | Connector not visible after deploy | Run `permissions add` to share | | Tests pass locally but deploy fails | Check environment variables are set correctly | | `globalConfigHttp` renamed | Revert — this variable name is required by Raw HTTP | | Workflow broken after version update | Manually fix property panel errors in each workflow |