Quick Start
This section gets you from zero to running your first WitEngine script and creating your first plugin. We'll cover the scripting basics, then show how to extend WitEngine with custom modules.
Part 1: Writing Scripts
Your First Script
Every WitEngine script defines a Job — the entry point for execution:
Job:HelloWorld()
{
Trace("Hello, WitEngine!");
}What's happening:
Job:HelloWorld()— Declares a job named "HelloWorld" with no parametersTrace(...)— Outputs a message (likeconsole.logorprint)
Variables and Types
WitEngine is strongly typed. Every variable has a declared type:
Job:Variables()
{
~ Declare variables with Type:name = value ~
Int:count = 42;
Double:pi = 3.14159;
String:name = "WitEngine";
Bool:isReady = true;
~ Use Trace to output values ~
Trace("Count:", count);
Trace("Pi:", pi);
Trace("Name:", name);
Trace("Ready:", isReady);
}Key points:
- Comments use tilde:
~ this is a comment ~ - Syntax:
Type:variableName = value; - All statements end with semicolon
Operations and Activities
In WitEngine, operations are called Activities. They're invoked with Module.Activity(args) syntax:
Job:BasicMath()
{
Int:a = 10;
Int:b = 3;
~ Arithmetic via activities ~
Int:sum = Int.Add(a, b); ~ 13 ~
Int:diff = Int.Subtract(a, b); ~ 7 ~
Int:product = Int.Multiply(a, b); ~ 30 ~
Int:quotient = Int.Divide(a, b); ~ 3 ~
Trace("Sum:", sum);
Trace("Difference:", diff);
Trace("Product:", product);
Trace("Quotient:", quotient);
}Why Int.Add(a, b) instead of a + b?
This is intentional — see 1.3 Design Philosophy. Every operation is explicit, making scripts easy to analyze and distribute.
Control Flow
Conditionals
Job:Conditionals()
{
Int:score = 85;
If(score >= 90)
{
Trace("Grade: A");
}
Else
{
If(score >= 80)
{
Trace("Grade: B");
}
Else
{
Trace("Grade: C or below");
}
}
}Loops
Fixed iteration:
Job:LoopExample()
{
~ Execute 5 times ~
Loop(5)
{
Trace("Iteration");
}
}Collection iteration:
Job:ForEachExample()
{
IntCollection:numbers = [1, 2, 3, 4, 5];
Int:sum = 0;
ForEach(n in numbers)
{
sum = Int.Add(sum, n);
Trace("Current sum:", sum);
}
Trace("Total:", sum);
}While loop:
Job:WhileExample()
{
Int:count = 0;
While(count < 5)
{
Trace("Count:", count);
count = Int.Add(count, 1);
}
}Collections
WitEngine provides typed collections for every primitive type:
Job:CollectionsDemo()
{
~ Create collections ~
IntCollection:numbers = [1, 2, 3, 4, 5];
StringCollection:names = ["Alice", "Bob", "Charlie"];
~ Get count ~
Int:count = Collection.Count(numbers);
Trace("Numbers count:", count);
~ Get element ~
String:first = Collection.First(names);
Trace("First name:", first);
~ Add element ~
names = Collection.Add(names, "Diana");
~ Create range ~
IntCollection:range = Int.Range(0, 10);
Trace("Range:", range);
}Parallel and Distributed Execution
Local Parallelism
Execute operations in parallel on a single machine:
Job:ParallelDemo()
{
IntCollection:items = Int.Range(1, 100);
~ Process items in parallel using all CPU cores ~
Parallel.ForEach(item in items)
{
Int:result = Int.Multiply(item, item);
Trace("Square of", item, "=", result);
}
}Distributed Execution
The Grid.ForEach activity distributes work across all connected nodes:
Job:DistributedDemo(IntCollection:data)
{
~ Configure distribution strategy ~
ProcessingOptions:opts = ProcessingOptions.Create("Queued");
opts = ProcessingOptions.SetTimeout(opts, 3600);
~ This distributes automatically across all nodes ~
ResultCollection:results = Grid.ForEach(item in data, opts)
=> Processor.ProcessItem(item);
Trace("Processed", Collection.Count(results), "items");
}Part 2: Creating Your First Plugin
WitEngine is extensible through plugins (also called controllers or modules). Here's how to create one.
Step 1: Create a .NET Project
Create a new .NET 8 class library:
dotnet new classlib -n MyPlugin -f net8.0
cd MyPluginAdd required NuGet packages:
dotnet add package MemoryPack
dotnet add package OutWit.Engine.InterfacesStep 2: Create the Controller
The controller is the entry point for your plugin. It declares metadata and registers services:
using OutWit.Engine.Interfaces;
using Microsoft.Extensions.DependencyInjection;
namespace MyPlugin
{
[WitPluginManifest(
"MyPlugin",
Version = "1.0.0",
Description = "My first WitEngine plugin"
)]
public class WitControllerMyPlugin : IWitControllerHost
{
public void Initialize(IServiceCollection services)
{
// Register adapters for variables and activities
services.AddSingleton<IWitVariableAdapter, MyVariableAdapter>();
services.AddSingleton<IWitActivityAdapter, MyActivityAdapter>();
}
}
}Step 3: Create a Custom Variable
Variables hold data in scripts. Create a simple Temperature type:
using MemoryPack;
using OutWit.Engine.Interfaces;
namespace MyPlugin
{
// Data class - must be MemoryPackable for distributed execution
[MemoryPackable]
public partial class Temperature
{
public double Value { get; set; }
public string Unit { get; set; } = "C";
}
// Variable wrapper
[Variable("Temperature")]
public class WitVariableTemperature : WitVariableTyped<Temperature>
{
public WitVariableTemperature() : base() { }
public WitVariableTemperature(Temperature value) : base(value) { }
}
}Step 4: Create a Simple Activity
Simple activities execute locally and return a result:
using OutWit.Engine.Interfaces;
namespace MyPlugin
{
[Function("Temperature", "Create")] // Called as Temperature.Create(...)
public class WitActivityTemperatureCreate : WitActivityFunction
{
// Define parameters
[WitParameter("value")]
public IWitParameter<double> Value { get; set; }
[WitParameter("unit", IsOptional = true, DefaultValue = "C")]
public IWitParameter<string> Unit { get; set; }
// Execution logic
protected override IWitVariable Execute(IWitVariablePool pool)
{
var temp = new Temperature
{
Value = Value.Value,
Unit = Unit.Value
};
return new WitVariableTemperature(temp);
}
}
}Now you can use it in scripts:
Job:TemperatureDemo()
{
Temperature:boiling = Temperature.Create(100, "C");
Temperature:freezing = Temperature.Create(32, "F");
Trace("Boiling point:", boiling);
Trace("Freezing point:", freezing);
}Step 5: Create a Transform Activity (for Distribution)
Transform activities can be distributed across nodes:
using OutWit.Engine.Interfaces;
namespace MyPlugin
{
[Transform("Temperature", "ConvertBatch")]
public class WitActivityTemperatureConvert : WitActivityTransform<Temperature, Temperature>
{
[WitParameter("targetUnit")]
public IWitParameter<string> TargetUnit { get; set; }
protected override Temperature Transform(Temperature input)
{
// Convert temperature to target unit
if (input.Unit == TargetUnit.Value)
return input;
double converted;
if (input.Unit == "C" && TargetUnit.Value == "F")
converted = input.Value * 9 / 5 + 32;
else if (input.Unit == "F" && TargetUnit.Value == "C")
converted = (input.Value - 32) * 5 / 9;
else
throw new ArgumentException($"Unknown conversion: {input.Unit} to {TargetUnit.Value}");
return new Temperature { Value = converted, Unit = TargetUnit.Value };
}
}
}Use it with distributed execution:
Job:ConvertTemperatures(TemperatureCollection:readings)
{
~ Convert all readings to Celsius, distributed across nodes ~
TemperatureCollection:celsius = Grid.ForEach(temp in readings)
=> Temperature.ConvertBatch(temp, "C");
Trace("Converted", Collection.Count(celsius), "readings");
}Step 6: Create the Variable Adapter
The adapter tells WitEngine how to parse your variable:
using OutWit.Engine.Interfaces;
namespace MyPlugin
{
public class MyVariableAdapter : IWitVariableAdapter
{
public IEnumerable<WitVariableInfo> GetVariables()
{
yield return new WitVariableInfo
{
Name = "Temperature",
Type = typeof(WitVariableTemperature),
CollectionType = typeof(WitVariableCollection<WitVariableTemperature>)
};
}
public IWitVariable Parse(string typeName, object value)
{
if (typeName == "Temperature" && value is Temperature temp)
return new WitVariableTemperature(temp);
return null;
}
}
}Step 7: Create the Activity Adapter
The adapter registers your activities:
using OutWit.Engine.Interfaces;
namespace MyPlugin
{
public class MyActivityAdapter : IWitActivityAdapter
{
public IEnumerable<WitActivityInfo> GetActivities()
{
yield return new WitActivityInfo
{
Module = "Temperature",
Name = "Create",
Type = typeof(WitActivityTemperatureCreate)
};
yield return new WitActivityInfo
{
Module = "Temperature",
Name = "ConvertBatch",
Type = typeof(WitActivityTemperatureConvert),
IsTransform = true
};
}
}
}Step 8: Build and Deploy
Build the plugin:
dotnet build -c ReleaseCopy the DLL to WitEngine's plugins folder and restart WitEngine to load the new plugin.
Plugin Architecture Summary
MyPlugin/
├── MyPlugin.csproj # Project file with NuGet references
├── WitControllerMyPlugin.cs # Plugin entry point [WitPluginManifest]
├── Types/
│ └── Temperature.cs # [MemoryPackable] data class
├── Variables/
│ └── WitVariableTemperature.cs # [Variable] wrapper
├── Activities/
│ ├── WitActivityTemperatureCreate.cs # [Function] - local execution
│ └── WitActivityTemperatureConvert.cs # [Transform] - distributed
└── Adapters/
├── MyVariableAdapter.cs # IWitVariableAdapter
└── MyActivityAdapter.cs # IWitActivityAdapterActivity Types Reference
| Type | Attribute | Use Case | Distributed |
|---|---|---|---|
| Function | [Function] |
Create objects, simple operations | No |
| Simple | [Simple] |
Side effects, no return value | No |
| Transform | [Transform] |
Process items in collection | Yes |
| Composite | [Composite] |
Complex multi-step operations | Configurable |
Inheritance Hierarchy
IWitActivity (interface)
├── WitActivitySimple → [Simple] activities
├── WitActivityFunction → [Function] activities
└── WitActivityTransform<TIn, TOut> → [Transform] activitiesComplete Example: Data Processing Pipeline
Here's a script that ties everything together:
Job:DataPipeline(StringCollection:inputFiles)
{
Trace("========================================");
Trace("Data Processing Pipeline");
Trace("========================================");
~ Step 1: Validate inputs ~
Int:fileCount = Collection.Count(inputFiles);
Trace("Input files:", fileCount);
If(fileCount == 0)
{
Trace("ERROR: No input files provided");
Return(false);
}
~ Step 2: Process files distributed across cluster ~
Trace("Processing files...");
ProcessingOptions:opts = ProcessingOptions.Create("Queued");
opts = ProcessingOptions.SetTimeout(opts, 300);
ResultCollection:results = Grid.ForEach(file in inputFiles, opts)
=> FileProcessor.ProcessFile(file);
~ Step 3: Aggregate results ~
Int:successCount = 0;
Int:failCount = 0;
ForEach(result in results)
{
If(result.Success == true)
{
successCount = Int.Add(successCount, 1);
}
Else
{
failCount = Int.Add(failCount, 1);
Trace("Failed:", result.FileName);
}
}
~ Step 4: Report ~
Trace("========================================");
Trace("Successful:", successCount);
Trace("Failed:", failCount);
Return(failCount == 0);
}