Portal Community
PropertyValue
Directive namecs
Required isolation levelTrusted (minimum)
ProjectScripting.Cs.Services
EngineMicrosoft.CodeAnalysis.CSharp.Scripting (Roslyn)
Syntax formTemplate literal: $cs`...script...`
Return typeAny — converted to string for expression output

Syntax

Inline expression
{@ $cs`Math.Round(Ctx.Input.Current.amount * 1.2m, 2)` }
Multi-line script with return
{@ $cs` var rate = decimal.Parse(Ctx.Env["VAT_RATE"] ?? "0.20"); var net = (decimal)Ctx.Input.Current.amount; return (net * rate).ToString("F2"); ` }
Trusted isolation required $cs requires Trusted isolation. This is the highest isolation level — only grant it to workflows authored by verified developers, not end-user-created workflows. Unlike $js (sandboxed Jint), C# scripts run as real .NET code with access to the full BCL.

Script Globals Reference

Scripts receive a Ctx globals object with pre-resolved context:

GlobalTypeEquivalent directive
Ctx.TenantITenantMetadata$ctx.tenant.*
Ctx.UserIUserClaims$ctx.user.*
Ctx.EnvIReadOnlyDictionary<string,string>$ctx.env.*
Ctx.NowDateTimeOffset$ctx.now
Ctx.InputINodeInputAccessor$input.*
Ctx.OutputINodeOutputAccessor$output.*
Ctx.ExecINodeExecutionFacts$exec.*
Ctx.VarsIExecutionMemory$var.*
Ctx.ServicesIServiceProvider

Complex Scenarios

Scenario 1 — HMRC PAYE Calculation with Decimal Precision

UK payroll tax requires precise decimal arithmetic — C# avoids floating-point issues inherent in JavaScript:

{@ $cs`
var gross     = (decimal)Ctx.Input.Current.grossPay;
var period    = (string)Ctx.Input.Current.payPeriod; // "monthly" | "weekly"
var annualised = period == "weekly" ? gross * 52m : gross * 12m;

var personalAllowance = 12570m;
decimal tax = 0m;
var taxable = Math.Max(annualised - personalAllowance, 0m);
if (taxable > 125140m) tax += (taxable - 125140m) * 0.45m;
if (taxable > 37700m)  tax += (Math.Min(taxable, 125140m) - 37700m) * 0.40m;
tax += Math.Min(taxable, 37700m) * 0.20m;

// Return period tax (not annualised)
var periodTax = period == "weekly" ? tax / 52m : tax / 12m;
return periodTax.ToString("F2");
` }

Scenario 2 — Regex-Based Document Classification

Classify an incoming document by scanning its reference field against known patterns:

{@ $cs`
using System.Text.RegularExpressions;
var reference = (string)(Ctx.Input.Current.reference ?? "");
if (Regex.IsMatch(reference, @"^INV-\d{4}-\d+$")) return "invoice";
if (Regex.IsMatch(reference, @"^PO-\d{6}$"))      return "purchase_order";
if (Regex.IsMatch(reference, @"^CR-\d+$"))         return "credit_note";
return "unknown";
` }

Scenario 3 — LINQ Aggregation Over Input Items

Use LINQ to aggregate statistics from a collection of payroll records:

{@ $cs`
using System.Linq;
using System.Text.Json;

var items  = Ctx.Input.Items; // IEnumerable<dynamic>
var totals = items
    .GroupBy(i => (string)i.department)
    .Select(g => new {
        Department = g.Key,
        HeadCount  = g.Count(),
        TotalGross = g.Sum(i => (decimal)i.grossPay)
    })
    .OrderByDescending(r => r.TotalGross)
    .ToList();

return JsonSerializer.Serialize(totals);
` }

Scenario 4 — Custom Service Injection via IServiceProvider

Resolve a registered DI service to perform domain-specific validation:

{@ $cs`
var svc = Ctx.Services.GetRequiredService<IPayrollRuleEngine>();
var employeeId = (int)Ctx.Input.Current.employeeId;
var period     = (string)Ctx.Input.Current.payPeriod;

var result = await svc.ValidateAsync(employeeId, period, CancellationToken.None);
return result.IsValid ? "valid" : string.Join("; ", result.Errors);
` }
// Async scripts are fully supported — just use await

Scenario 5 — Cryptographic Hash for Idempotency

Generate a deterministic SHA-256 hash from key fields to detect duplicate submissions:

{@ $cs`
using System.Security.Cryptography;
using System.Text;

var key = string.Concat(
    Ctx.Input.Current.supplierId, "|",
    Ctx.Input.Current.invoiceNumber, "|",
    Ctx.Input.Current.invoiceDate
);
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(key));
return Convert.ToHexString(bytes).ToLower();
` }
// Deterministic key — same invoice always produces same hash

Scenario 6 — XML Parsing for Legacy System Integration

Parse an XML response from a legacy system and extract a specific field:

{@ $cs`
using System.Xml.Linq;

var xmlString = (string)Ctx.Output["LegacySystem"].body;
var doc       = XDocument.Parse(xmlString);
var ns        = XNamespace.Get("http://schemas.acme.com/payroll/v1");

var employeeId = doc
    .Descendants(ns + "Employee")
    .FirstOrDefault()
    ?.Attribute("ID")
    ?.Value;

return employeeId ?? throw new InvalidOperationException("Employee ID not found in XML response");
` }

Common Errors

ErrorCauseFix
IsolationViolation: $cs requires TrustedNode isolation level is Safe or SandboxedSet node isolation to Trusted; restrict to developer-authored workflows
CompilationError: CS0234Missing using statement for a namespaceAdd the using statement inside the script block
RuntimeError: InvalidCastExceptionDynamic property cast to wrong typeUse safe casts: (decimal?)(double?)Ctx.Input.Current.amount ?? 0m
Script hangsAsync deadlock or service call without cancellationAlways pass CancellationToken.None (or the provided token) to async calls
ServiceNotFound: IMyServiceService not registered in DIRegister the service in AddExpressionEvaluation() or app DI setup