docs(elsa): add scripts sub-article (JavaScript/Jint and C#)
This commit is contained in:
parent
e541255aa5
commit
f05da6a583
|
|
@ -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.
|
||||
|
|
|
|||
190
elsa/scripts.md
Normal file
190
elsa/scripts.md
Normal file
|
|
@ -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<T>("name")` |
|
||||
| Output of an earlier activity | `getOutputFrom("Activity", "Result")` | `Output.From("Activity", "Result")` / `Output.From<T>(...)` |
|
||||
| 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.<name>` | 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<Name>()` / `set<Name>(value)` where `<Name>` 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<inputName>()` — the accessor keeps the **exact**
|
||||
input name (`name` → `getname()`).
|
||||
- **Activity outputs**: `get<Output>From<ActivityName>()`, 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<T>(name)`, `SetVariable(name, value)`, `GetInput<T>(name)`, and `.ExpressionExecutionContext` for the raw context |
|
||||
| `Variable` (alias `Variables`) | `WorkflowVariablesProxy` | `Get(name)`, `Get<T>(name)`, `Set(name, value)` + a typed property per variable |
|
||||
| `Input` (alias `Inputs`) | `WorkflowInputsProxy` | `Get<T>(name)` + a typed property per workflow input |
|
||||
| `Output` | `OutputProxy` | `From(activity, outputName)`, `From<T>(...)`, `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<string, object>` | 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>("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.
|
||||
Loading…
Reference in a new issue