diff --git a/elsa/README.md b/elsa/README.md index 78c5ee5..df5cf3b 100644 --- a/elsa/README.md +++ b/elsa/README.md @@ -17,5 +17,6 @@ Custom elements authored in the docs repository are preserved by the renderer to ## In this section - **Getting started** — install nothing: open **Elsa Flows** and create your first definition. -- **Workflows** — definitions, versions and publishing. - **Runs & instances** — dispatch a workflow and inspect its execution. +- **Scripts: JavaScript & C#** — every function, variable, input, output and outcome you can call from a script. +- **Workflows** — definitions, versions and publishing. diff --git a/elsa/scripts.md b/elsa/scripts.md new file mode 100644 index 0000000..116c52d --- /dev/null +++ b/elsa/scripts.md @@ -0,0 +1,190 @@ +# Scripts: JavaScript & C# + +Elsa can run code inside a workflow two ways: + +1. **Script activities** — `Run JavaScript` (`Elsa.RunJavaScript`) and `Run C#` + (`Elsa.RunCSharp`). Both take a `Script` input and produce a `Result` output. +2. **Inline expressions** — any activity input can hold an expression instead of + a literal. The syntax is chosen per value: `JavaScript`, `CSharp` or + `Liquid`. + +Both routes evaluate against the **current execution context**, so they can read +workflow inputs, read/write workflow variables and read the outputs of +activities that already ran. This page lists exactly what is callable. + +> JavaScript runs on the **Jint** engine. C# runs on **Roslyn** scripting. The +> lists below are taken from the engine source (Elsa `3.8.2`, +> `elsa-core/src/modules/Elsa.Expressions.JavaScript` and +> `elsa-core/src/modules/Elsa.Expressions.CSharp`). Only what is listed here is +> wired by this host — in particular JS has **no** `console`, **no** CLR access +> and **no** `getConfig` (see [Limits](#limits)). + +## Reading and writing data + +| What you want | JavaScript | C# | +| --- | --- | --- | +| Read a workflow variable | `getVariable("Name")` | `ExecutionContext.GetVariable("Name")` / `Variable.Get("Name")` | +| Write a workflow variable | `setVariable("Name", value)` | `ExecutionContext.SetVariable("Name", value)` / `Variable.Set("Name", value)` | +| Variable as a property | `variables.Name` | `Variable.Name` (typed, see note) | +| Read a workflow input | `getInput("name")` | `Input.Get("name")` | +| Output of an earlier activity | `getOutputFrom("Activity", "Result")` | `Output.From("Activity", "Result")` / `Output.From(...)` | +| Result of the previous activity | `getLastResult()` | `Output.LastResult` | +| Current workflow instance id | `getWorkflowInstanceId()` | `WorkflowInstanceId` | +| Correlation id | `getCorrelationId()` / `setCorrelationId(v)` | `CorrelationId` (get/set) | +| Workflow instance name | `getWorkflowInstanceName()` / `setWorkflowInstanceName(v)` | `WorkflowInstanceName` (get/set) | +| Definition id / version | `getWorkflowDefinitionId()`, `getWorkflowDefinitionVersionId()`, `getWorkflowDefinitionVersion()` | — (use the workflow variables/inputs instead) | + +Notes: + +- `getVariable` / `Variable.Get` are **exact-name**. The typed C# property + `Variable.Name` is generated from the *Pascalized* variable name and looks the + variable up by that Pascalized name, so use `Variable.Get("exactName")` when a + variable is not already PascalCase. +- The **activity reference** in `getOutputFrom` / `Output.From` is the activity's + **name or id** (name is what the designer shows). The output name is the one + from the activity descriptor — `Result` for code activities such as + `W4C.W4cEcho`, `W4C.RunSql`, `Elsa.RunJavaScript`. +- Reading an output of an activity that has not run yet returns `null`. + +## JavaScript API + +### Built-in functions + +Workflow / context: + +| Function | Returns | +| --- | --- | +| `getWorkflowDefinitionId()` | definition id (string) | +| `getWorkflowDefinitionVersionId()` | definition version id (string) | +| `getWorkflowDefinitionVersion()` | version number (int) | +| `getWorkflowInstanceId()` | workflow instance id (string) | +| `getCorrelationId()` / `setCorrelationId(value)` | correlation id | +| `getWorkflowInstanceName()` / `setWorkflowInstanceName(value)` | instance name | +| `getInput(name)` | workflow input value | +| `getVariable(name)` / `setVariable(name, value)` | variable value | +| `variables.` | variable value (write-back supported) | +| `getOutputFrom(activity, outputName)` | output of another activity | +| `getLastResult()` | result of the previous activity | + +Helpers: + +| Function | Purpose | +| --- | --- | +| `toJson(value)` | JSON string (enums as strings, full Unicode) | +| `isNullOrEmpty(value)` / `isNullOrWhiteSpace(value)` | string checks | +| `parseGuid(value)` | `Guid` from string | +| `newGuid()` / `newGuidString()` / `newShortGuid()` | new Guids | +| `bytesToString` / `bytesFromString` | UTF-8 byte ↔ string | +| `bytesToBase64` / `bytesFromBase64` | byte[] ↔ base64 | +| `stringToBase64` / `stringFromBase64` | string ↔ base64 | +| `streamToBytes(stream)` / `streamToBase64(stream)` | Stream → bytes/base64 | + +Outcomes (only meaningful in `Run JavaScript`, whose `Script` input supports +them): `setOutcome(name)` and `setOutcomes(names[])`. If neither is called the +activity completes with the default outcome **`Done`**. Declare the possible +names in the activity's `PossibleOutcomes` input so they appear on the node. + +Deprecated but present: `getGuidString()`, `getShortGuid()`. + +### Generated accessors + +For convenience the engine also injects name-based accessors: + +- **Variables**: `get()` / `set(value)` where `` is the + variable name **Pascalized** (`counter` → `getCounter()`). These use the real + variable name internally, so they are reliable for any variable name. +- **Workflow inputs**: `get()` — the accessor keeps the **exact** + input name (`name` → `getname()`). +- **Activity outputs**: `getFrom()`, both Pascalized + (`Result` from activity `Echo` → `getResultFromEcho()`). + +The explicit functions above are the stable API; the accessors are a shorthand. + +### Registered .NET types + +`DateTime`, `DateTimeOffset`, `TimeSpan`, `Guid`, `Random`, +`LogPersistenceMode`, plus the types of the workflow's declared variables. They +are usable as static classes, e.g. `DateTime.UtcNow`, `Guid.NewGuid()`. + +### Values and results + +- The value of the **last expression**, or an explicit top-level `return` + (`return` outside a function is allowed), becomes the script result. +- A returned Promise is awaited/unwrapped by the engine. +- If the result is not `null`, `Run JavaScript` writes it to the `Result` output. + +```js +// totals an order, stores it in a variable and returns a JSON summary +const order = getInput("order"); +const total = order.items.reduce((sum, item) => sum + item.price, 0); +setVariable("Total", total); + +const echo = getOutputFrom("Echo", "Result"); // result of an earlier node +if (total > 1000) setOutcome("Rejected"); else setOutcome("Approved"); + +return toJson({ total, echo }); +``` + +## C# API + +The script is a Roslyn C# script. Its globals are an instance of +`Elsa.Expressions.CSharp.Models.Globals`, which exposes: + +| Global | Type | Use | +| --- | --- | --- | +| `ExecutionContext` | `ExecutionContextProxy` | `GetVariable(name)`, `GetVariable(name)`, `SetVariable(name, value)`, `GetInput(name)`, and `.ExpressionExecutionContext` for the raw context | +| `Variable` (alias `Variables`) | `WorkflowVariablesProxy` | `Get(name)`, `Get(name)`, `Set(name, value)` + a typed property per variable | +| `Input` (alias `Inputs`) | `WorkflowInputsProxy` | `Get(name)` + a typed property per workflow input | +| `Output` | `OutputProxy` | `From(activity, outputName)`, `From(...)`, `LastResult` | +| `Outcome` | `OutcomeProxy` | `Set(params string[] outcomeNames)` | +| `Args` | `ArgumentsProxy` | arguments passed by the caller of the evaluator (`Run C#` passes none — advanced/internal) | +| `WorkflowInstanceId` | `string` | workflow instance id | +| `CorrelationId` | `string?` (get/set) | correlation id | +| `WorkflowInstanceName` | `string?` (get/set) | instance name | +| `Arguments` | `IDictionary` | raw argument dictionary | + +Result and outcomes: + +- The script's last expression / `return` value becomes the script result; when + not `null` it is written to the `Run C#` `Result` output. +- Call `Outcome.Set("Approved")` (or `Outcome.Set("Approved", "Rejected")`) to + choose an outcome; the default is **`Done`**. Declare them in + `PossibleOutcomes`. +- `await` works in the script. + +Imports and references: + +- Default namespaces: `Elsa.Expressions.CSharp`, `System`, `System.Linq`, + `System.Collections.Generic`, `System.Dynamic`, `System.Text.Json`, + `System.Text.Json.Serialization`, `System.Text.Json.Nodes`. +- Default assemblies: Elsa.Expressions.CSharp, System.Linq/System.Runtime, + System.Text.Json, System.Collections, System.Linq.Expressions + (`ExpandoObject`) and Microsoft.CSharp (dynamic). Nothing else (no + `System.Net.Http`, no `System.IO`) unless the host adds it. + +```csharp +var order = Input.Get("order"); +var total = order.Items.Sum(item => item.Price); +Variable.Total = total; // typed var (PascalCase) +ExecutionContext.SetVariable("Total", total); // exact-name API + +var echo = Output.From("Echo", "Result"); +Outcome.Set(total > 1000 ? "Rejected" : "Approved"); + +return JsonSerializer.Serialize(new { total, echo }); +``` + +## Limits + +- **JavaScript is the safe sandbox.** This host calls `UseJavaScript()` without + options, so `AllowClrAccess` and `AllowConfigurationAccess` stay `false`: + there is no `clr`, no `getConfig`, and no `console`. The extra JS libraries + (lodash, moment) are **not** installed. The only extension points are the + registered types and the built-ins listed above. +- **C# is not a sandbox.** `Scripting:CSharp:AllowHostCodeExecution` is `true` in + this deployment (including production), so `Run C#` can run arbitrary code in + the Elsa host process — filesystem, network, DI services and every tenant's + data. Only trusted authors should be allowed to edit flows. Turning the flag + off removes `Run C#` and the `CSharp` syntax from the palette entirely. +- Both engines cache compiled scripts for one day (sliding), and both run + in-process with the caller's cancellation token.