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 parameters
  • Trace(...) — Outputs a message (like console.log or print)

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:

bash
dotnet new classlib -n MyPlugin -f net8.0
cd MyPlugin

Add required NuGet packages:

bash
dotnet add package MemoryPack
dotnet add package OutWit.Engine.Interfaces

Step 2: Create the Controller

The controller is the entry point for your plugin. It declares metadata and registers services:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

bash
dotnet build -c Release

Copy 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     # IWitActivityAdapter

Activity 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] activities

Complete 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);
}