diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a8739a7..d301097 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "genesys-cloud-architect", "source": "./", "description": "Create, test, and debug Genesys Cloud Architect flows", - "version": "1.0.3" + "version": "1.0.4" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1364be0..59cbabf 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "genesys-cloud-architect", "description": "Create, test, and debug Genesys Cloud Architect flows", - "version": "1.0.3", + "version": "1.0.4", "author": { "name": "Lucas Woodward", "url": "https://makingchatbots.com/" diff --git a/package.json b/package.json index 6bc266c..a1002e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "genesys-cloud-architect", - "version": "1.0.3", + "version": "1.0.4", "private": true, "packageManager": "pnpm@11.1.3+sha512.c85357fe17ca12dd23dd7071822666dfd7e3cb76fe214e3370b5ea2fb34f2a231185509b63e717f3cd0acb38dd3f8d82bcd5e8172400ae678b70ea4fbed0896d", "scripts": { diff --git a/servers/genesys-cloud-architect-mcp.js b/servers/genesys-cloud-architect-mcp.js index dfed348..027c2d6 100644 --- a/servers/genesys-cloud-architect-mcp.js +++ b/servers/genesys-cloud-architect-mcp.js @@ -159,7 +159,7 @@ ${s.warnings.join(` `)}`),l({content:[{type:"text",text:f.join(` `)+m}]})}else{let f=s?.error??`Deploy runner exited with code ${h}`;l({isError:!0,content:[{type:"text",text:`Deploy failed: ${f}${m}`}]})}})})}});function pI(t){let e=t.toUpperCase();return e.endsWith("FLOW")?e:`${e}FLOW`}function iX(t,e){let i={};for(let n of e){let a=n.type??"UNKNOWN";i[a]||(i[a]=[]),i[a].push({id:n.id??"",name:n.name??"",...n.version?{version:n.version}:{},deleted:n.deleted??!1,updated:n.updated??!1})}return{flow:{id:t.id??"",name:t.name,type:pI(t.type??""),version:t.publishedVersion?.commitVersion??"1"},dependencies:i}}var dI=({architectApi:t})=>({config:{description:"Retrieves all dependencies consumed by a Genesys Cloud Architect flow. Returns the flow metadata and its dependencies grouped by type.",annotations:{title:"Flow Dependencies",readOnlyHint:!0,destructiveHint:!1},inputSchema:{flowId:Qi.string().min(1).describe("The Genesys Cloud Architect flow ID")}},handler:async({flowId:e})=>{try{let i;try{i=await t.getFlow(e)}catch{return{isError:!0,content:[{type:"text",text:`Flow "${e}" not found.`}]}}let n=pI(i.type??""),a=i.publishedVersion?.commitVersion??"1",r=[],s=1;for(;;){let l=await t.getArchitectDependencytrackingConsumedresources(i.id,a,n,{pageSize:100,pageNumber:s});if(l.entities&&r.push(...l.entities),!l.nextUri)break;s++}let o=iX(i,r);return{content:[{type:"text",text:JSON.stringify(o,null,2)}]}}catch(i){return{isError:!0,content:[{type:"text",text:`Failed to retrieve flow dependencies: ${i instanceof Error?i.message:String(i)}`}]}}}});var ry=Qi.object({GENESYS_REGION:Qi.string().min(1),GENESYS_CLIENT_ID:Qi.string().min(1),GENESYS_CLIENT_SECRET:Qi.string().min(1),DEPLOY_SCRIPT_PATH:Qi.string().min(1),PREVENT_LOGIN:Qi.enum(["TRUE","FALSE"]).default("FALSE").transform(t=>t==="TRUE")}).safeParse(process.env);if(!ry.success){let t=ry.error.issues.map(e=>e.path[0]).join(` `);console.error(`Missing required environment variables: - ${t}`),process.exit(1)}var Kt=ry.data,oy=new Cc({name:"genesys-cloud-architect",version:"1.0.3"}),hI=dI({architectApi:new sy.default.ArchitectApi});oy.registerTool("flow_dependencies",hI.config,hI.handler);var gI=cI({region:Kt.GENESYS_REGION,clientId:Kt.GENESYS_CLIENT_ID,clientSecret:Kt.GENESYS_CLIENT_SECRET,deployScriptPath:Kt.DEPLOY_SCRIPT_PATH});oy.registerTool("deploy_flow",gI.config,gI.handler);(async()=>{if(Kt.PREVENT_LOGIN)console.warn("Login for Platform API skipped. Calling tools will result in an auth failure.");else{let e=sy.default.ApiClient.instance;e.setEnvironment(Kt.GENESYS_REGION),await e.loginClientCredentialsGrant(Kt.GENESYS_CLIENT_ID,Kt.GENESYS_CLIENT_SECRET)}let t=new bc;await oy.connect(t)})().catch(t=>{console.error("Failed to start server:",t),process.exit(1)}); + ${t}`),process.exit(1)}var Kt=ry.data,oy=new Cc({name:"genesys-cloud-architect",version:"1.0.4"}),hI=dI({architectApi:new sy.default.ArchitectApi});oy.registerTool("flow_dependencies",hI.config,hI.handler);var gI=cI({region:Kt.GENESYS_REGION,clientId:Kt.GENESYS_CLIENT_ID,clientSecret:Kt.GENESYS_CLIENT_SECRET,deployScriptPath:Kt.DEPLOY_SCRIPT_PATH});oy.registerTool("deploy_flow",gI.config,gI.handler);(async()=>{if(Kt.PREVENT_LOGIN)console.warn("Login for Platform API skipped. Calling tools will result in an auth failure.");else{let e=sy.default.ApiClient.instance;e.setEnvironment(Kt.GENESYS_REGION),await e.loginClientCredentialsGrant(Kt.GENESYS_CLIENT_ID,Kt.GENESYS_CLIENT_SECRET)}let t=new bc;await oy.connect(t)})().catch(t=>{console.error("Failed to start server:",t),process.exit(1)}); /*! Bundled license information: mime-db/index.js: diff --git a/skills/write-flow/SKILL.md b/skills/write-flow/SKILL.md index 59daf0a..5e0dc3d 100644 --- a/skills/write-flow/SKILL.md +++ b/skills/write-flow/SKILL.md @@ -53,7 +53,8 @@ Before writing any flow code, read these reference files from this skill: 1. **Always read**: `references/sdk-patterns.md` — core SDK patterns and the `buildFlow` contract 2. **Always read**: `references/gotchas.md` — SDK quirks that cause silent failures 3. **Always read**: `references/action-reference.md` — available action types -4. **Read the matching example** from `references/examples/`: +4. **Read when the flow uses expressions**: `references/expression-reference.md` — expression language syntax, functions, operators, data types, and common patterns. Read this whenever the flow involves conditional logic, dynamic text, variable manipulation, date/time calculations, or any `setExpression()` / expression property usage. +5. **Read the matching example** from `references/examples/`: - `inbound-call.md` — IVR with menus and queue transfers - `inbound-chat.md` — chat greeting and queue transfer - `inbound-email.md` — auto-reply and queue transfer diff --git a/skills/write-flow/references/expression-reference.md b/skills/write-flow/references/expression-reference.md new file mode 100644 index 0000000..2de5072 --- /dev/null +++ b/skills/write-flow/references/expression-reference.md @@ -0,0 +1,230 @@ +# Architect Expression Language + +Expressions are string values passed to SDK methods like `setExpression()`, `variableValue`, `numberExpression`, etc. They use Genesys Cloud's own expression syntax — not TypeScript. + +## Syntax Rules + +- **String literals**: double quotes inside the expression — `'"Hello"'` in TypeScript +- **Escape sequences**: `\"` `\\` `\t` `\n` `\r` +- **Decimal separator**: always `.` (culture-invariant) +- **Property access**: dot notation — `Flow.myJson.userName` +- **Collection access**: brackets — `Task.Queues[Task.Index]` +- **NOT_SET**: special "undefined" value, distinct from `""`. Check with `IsSet()` or `IsNotSetOrEmpty()` + +## Operators (highest → lowest precedence) + +| Precedence | Operators | Notes | +|---|---|---| +| 1 | `[]` `.` | Collection/property access | +| 2 | `-` `!` `~` | Unary minus, logical NOT, bitwise NOT | +| 3 | `^` | Exponentiation (`2^3 = 8`) | +| 4 | `*` `/` `%` | Multiply, divide, modulus | +| 5 | `+` `-` | Add, subtract, string concat | +| 6 | `<` `<=` `>` `>=` | Comparison | +| 7 | `==` `!=` | Equality | +| 8 | `&` | Bitwise AND | +| 9 | `\|` | Bitwise OR | +| 10 | `and` | Logical AND | +| 11 | `or` | Logical OR | + +Parentheses `()` override precedence. + +## Data Types + +### Scalar types + +| Type | Range / Notes | +|---|---| +| Boolean | `true` / `false` | +| Integer | -999,999,999,999,999 to 999,999,999,999,999 | +| Decimal | Up to 40 digits precision | +| String | Double-quoted text | +| DateTime | Jan 1 1800 – Dec 31 2200, UTC only | +| Date | Date only (no time) | +| Time | Time only (no date) | +| Duration | Milliseconds (-999T to 999T) | +| Currency | Decimal amount + ISO 4217 code (`MakeCurrency(5.25, "USD")`) | +| Phone Number | Has `.dialingCode` property | +| JSON | Objects, arrays, strings, numbers, booleans, null | + +### Network types (org-specific, cannot be created from strings) +Queue, User, Group, ACD Skill, Language Skill, Schedule, Schedule Group, Emergency Group, Wrapup Code + +### Collections +Most types have collection variants (String Collection, Integer Collection, Queue Collection, etc.). Access items with `GetAt(collection, index)` (0-based). + +### Special values +- `NOT_SET` — undefined, checked with `IsSet()` / `IsNotSetOrEmpty()` +- `System.MinInt` / `System.MaxInt` — integer boundaries +- `System.MinDateTime` / `System.MaxDateTime` — datetime boundaries + +### Implicit casting +Architect auto-converts between compatible types. Assigning `3+4` (Integer) to a String variable wraps it as `ToString(3+4)`. + +## Variable Scoping + +| Prefix | Scope | +|---|---| +| `Flow.` | Entire flow | +| `Task.` | Current task only | +| `State.` | Current state only | +| `Call.` | Built-in, inbound/outbound calls (read-only) | +| `Email.` | Built-in, email flows (read-only) | +| `Message.` | Built-in, messaging flows (read-only) | +| `Chat.` | Built-in, chat flows (read-only) | +| `Session.` | Built-in, bot/survey flows (read-only) | +| `System.` | System constants | + +### Common built-in variables + +| Variable | Type | Description | +|---|---|---| +| `Call.Ani` | String | Caller's phone number | +| `Call.CalledAddress` | String | Dialed number | +| `Call.ConversationID` | String | Conversation ID | +| `Call.Language` | String | IETF language tag | +| `Call.CurrentQueue` | Queue | Current queue | +| `Flow.IsTest` | Boolean | Debug mode flag | +| `Flow.StartDateTimeUtc` | DateTime | When the flow started | + +## Functions + +### String + +| Function | Description | +|---|---| +| `Append(s1, s2, ...)` | Concatenate (preferred over `+`, handles NOT_SET) | +| `Substring(str, start, length)` | Extract substring | +| `IndexOf(str, search)` | Find position (0-based, -1 if not found) | +| `Contains(str, search)` | Check if substring exists → Boolean | +| `Replace(str, find, replaceWith)` | Replace all occurrences | +| `ToUpper(str)` | Uppercase | +| `ToLower(str)` | Lowercase | +| `Trim(str)` | Remove leading/trailing whitespace | +| `Length(str)` | String length | + +### DateTime + +| Function | Description | +|---|---| +| `GetCurrentDateTimeUtc()` | Current UTC date/time | +| `MakeDateTime(y, m, d, h, min, s)` | Construct a DateTime | +| `Year(dt)` / `Month(dt)` / `Hour(dt)` | Extract components | +| `DayOfWeek(dt)` | Day of week (integer) | +| `AddHours(dt, n)` | Add/subtract hours | +| `AddMinutes(dt, n)` | Add/subtract minutes | +| `AddDays(dt, n)` | Add/subtract days | + +### Type Conversion + +| Function | Description | +|---|---| +| `ToString(value)` | Convert to string (appends "Z" for DateTime) | +| `ToInt(str)` | Parse string to integer | +| `ToDecimal(value)` | Convert to decimal | +| `ToDateTime(str)` | Parse string to DateTime | +| `ToDuration(value)` | Convert to duration | +| `ToPhoneNumber(str)` | Convert to PhoneNumber (enables `.dialingCode`) | +| `ToJson(value)` | Convert singleton to JSON | +| `JsonParse(str)` | Parse JSON string (strict: double quotes required) | +| `ToBoolean(value)` | Convert to boolean | + +### Logic + +| Function | Description | +|---|---| +| `If(cond, trueVal, falseVal)` | Conditional (supports nesting) | +| `IsSet(value)` | True if value is not NOT_SET | +| `IsNotSetOrEmpty(value)` | True if NOT_SET or empty string | + +### Collections + +| Function | Description | +|---|---| +| `Count(collection)` | Number of items | +| `GetAt(collection, index)` | Item at index (0-based) | +| `AddItem(collection, item)` | Append item | +| `RemoveItem(collection, value)` | Remove by value | +| `RemoveItemAt(collection, index)` | Remove by index | +| `Find(collection, value)` | Find items → collection of matches | +| `FindFirst(collection, value)` | First index (-1 if not found) | +| `RemoveDups(collection)` | Remove duplicates | + +### Audio / TTS (voice flows only) + +| Function | Description | +|---|---| +| `ToAudioTTS(text)` | Text to speech | +| `ToAudioBlank(duration)` | Silent pause | +| `ToAudioNumber(number)` | Read number aloud | +| `ToAudioPhoneNumber(phone)` | Read phone number aloud | + +### Currency + +| Function | Description | +|---|---| +| `MakeCurrency(amount, code)` | Create currency (`MakeCurrency(5.25, "USD")`) | + +## Common Patterns + +### Time-of-day greeting +``` +If(Hour(GetCurrentDateTimeUtc()) < 12, "Good morning", If(Hour(GetCurrentDateTimeUtc()) < 17, "Good afternoon", "Good evening")) +``` + +### Safe variable access (NOT_SET guard) +``` +5 + If(IsSet(Flow.MyInteger), Flow.MyInteger, 0) +``` + +### String concatenation (safe) +``` +Append("Hello ", Flow.CustomerName, "!") +``` + +### Check if caller is in a blocked list +``` +Count(Find(Task.BlockedNumbers, ToPhoneNumber(Call.Ani))) > 0 +``` + +### Collection lookup with fallback +``` +If(Task.FoundIndex != -1, GetAt(Task.Queues, Task.FoundIndex), Task.DefaultQueue) +``` + +### Business hours check +``` +Hour(GetCurrentDateTimeUtc()) >= 8 and Hour(GetCurrentDateTimeUtc()) <= 17 +``` + +### JSON property access +``` +Flow.MyJson.userName +``` + +### Phone number dialing code +``` +ToPhoneNumber(Call.Ani).dialingCode +``` + +## Expression Quoting in the SDK + +Expressions are strings inside strings. The outer quotes are TypeScript, the inner quotes are expression syntax: + +```typescript +// String literal in an expression — double quotes inside single quotes +response.messageBody.setExpression('"Hello!"'); + +// Variable reference — no inner quotes needed +setVar.variableValue = "Flow.customerName"; + +// Function call with string argument +response.messageBody.setExpression('Append("Hello ", Flow.Name, "!")'); + +// Conditional with string results +comm.communicationExpression.setExpression( + 'If(Hour(GetCurrentDateTimeUtc()) < 12, "Good morning", "Good afternoon")' +); +``` + +Only string literals inside expressions need the inner double quotes. Variable references, function calls returning non-strings, and numeric literals do not. \ No newline at end of file