docs(elsa): add scripts sub-article (JavaScript/Jint and C#)

This commit is contained in:
docs 2026-09-25 13:45:07 +03:00
parent e541255aa5
commit f05da6a583
2 changed files with 192 additions and 1 deletions

View file

@ -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
View 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.