Embedding

Run the Argyll engine inside a Go application, with its own storage, Step handlers, and Flows.

The github.com/kode4food/argyll/engine/pkg/argyll package runs the full engine in your process. The application owns storage, decides which Step types exist, registers Steps, and starts Flows through ordinary Go calls. Everything the standalone server does over HTTP is available as a method on the embedded Engine.

Create and Start

argyll.New builds an engine over a timebox backend. Nothing runs until Start, which begins scheduling Work and recovers any Flows that were interrupted. Stop shuts the engine down and closes the stores it opened:

import (
    "github.com/kode4food/timebox"
    "github.com/kode4food/timebox/memory"

    "github.com/kode4food/argyll/engine/pkg/argyll"
)

func start() (argyll.Engine, error) {
    eng, err := argyll.New(argyll.Options{
        Backend: func(pub timebox.Publisher) (timebox.Backend, error) {
            return memory.Open(memory.Config{Publisher: pub}), nil
        },
    })
    if err != nil {
        return nil, err
    }
    return eng, eng.Start()
}

Backend opens the store the engine runs on, wired to the publisher its committed events reach. The memory backend suits tests and single-process tools; timebox also provides postgres, redis, and raft backends for durable and clustered deployments.

OptionEffect
BackendOpens the timebox backend the engine runs on
ConfigEngine configuration, defaulting to config.NewDefaultConfig()
HandlersThe Step types the engine runs, defaulting to the built-in Script, Service, and Flow handlers
CallbackBuilds the callback URL an async Service Step reports its outcome to

Register Steps and Run a Flow

Steps, Spaces, and Flows use the same types as the HTTP API, from github.com/kode4food/argyll/engine/pkg/api:

err := eng.RegisterStep(&api.Step{
    ID:   "greet",
    Name: "Greet",
    Type: api.StepTypeScript,
    Script: &api.ScriptConfig{
        Language: api.ScriptLangLua,
        Script: `
            return { greeting = "hello " .. name }
        `,
    },
    Attributes: api.AttributeSpecs{
        "name":     {Role: api.RoleRequired, Type: api.TypeString},
        "greeting": {Role: api.RoleOutput, Type: api.TypeString},
    },
})

err = eng.StartFlow(api.CreateFlowRequest{
    ID:    "greet-ada",
    Goals: []api.StepID{"greet"},
    Init:  api.InitArgs{"name": {"ada"}},
})

fl, err := eng.GetFlowState("greet-ada")

RegisterSteps registers several Steps in one transaction, so a conflict among them leaves the catalog as it was. QueryFlows, ListFlows, and GetFlowStatus report on Flows, and the Space methods scope planning exactly as the Spaces API does.

Step Handlers

A Step’s type selects the handler that runs it. Handlers maps each Step type to a step.Handler, and the map you pass is the exact set the engine runs. builtins.All returns the built-in handlers, and With returns a copy extended by your own:

import (
    "time"

    "github.com/kode4food/argyll/engine/pkg/api"
    "github.com/kode4food/argyll/engine/pkg/step"
    "github.com/kode4food/argyll/engine/pkg/step/builtins"
)

greet := &step.Handler{
    Invoke: func(
        rt step.Runtime, st *api.Step, args api.Args, tkn api.Token,
    ) error {
        name, _ := args["name"].(string)
        return rt.CompleteWork(tkn, api.Args{"greeting": "hello " + name})
    },
}

client := builtins.NewHTTPClient(30 * time.Second)
handlers := builtins.All(client, nil)

eng, err := argyll.New(argyll.Options{
    Backend:  open,
    Handlers: handlers.With(step.Handlers{"greet": greet}),
})

With accepts any number of handler sets, and a later set replaces a Step type an earlier one registers. It leaves the handlers it extends unchanged.

A Step whose type is greet now runs in process, with no configuration beyond its Attributes:

err = eng.RegisterStep(&api.Step{
    ID:   "greet",
    Name: "Greet",
    Type: "greet",
    Attributes: api.AttributeSpecs{
        "name":     {Role: api.RoleRequired, Type: api.TypeString},
        "greeting": {Role: api.RoleOutput, Type: api.TypeString},
    },
})

A handler supplies whichever capabilities its Step type needs:

FieldCapability
InvokeRuns one Work Item. Call rt.CompleteWork with the outputs, or return an error to fail the Work Item
CompensateReverses a completed Work Item for Steps with compensated handling
ValidateChecks Step configuration when the Step is registered
HealthReports whether the Step can run, recorded when it is registered
ChildrenNames the Steps a Step expands into, as Flow Steps do

Invoke receives the Work Item’s inputs by their invocation names, and step.Runtime provides the Flow ID, Step ID, and Flow metadata. An error that wraps api.ErrWorkNotCompleted leaves the Work Item for retry; any other error fails it.

A handler can also finish Work later. Return from Invoke without completing, keep the Flow ID, Step ID, and token, and report the outcome through the engine’s CompleteWork or FailWork when the Work is done.

The Go Step Generator writes these handlers for you from ordinary Go functions.

Async Service Steps

An async Service Step reports its outcome to a callback URL. Set Callback to build those URLs below an address your application serves, and route that address to builtins.Callback:

eng, err := argyll.New(argyll.Options{
    Backend:  open,
    Callback: builtins.BaseCallbackURL("https://orders.example.com"),
})

mux := http.NewServeMux()
mux.HandleFunc("POST /callbacks/{flow_id}/{step_id}/{token}/{action}",
    func(w http.ResponseWriter, r *http.Request) {
        status, err := builtins.Callback(eng, &builtins.CallbackRequest{
            FlowStep: api.FlowStep{
                FlowID: api.FlowID(r.PathValue(api.ParamFlowID)),
                StepID: api.StepID(r.PathValue(api.ParamStepID)),
            },
            Token:   api.Token(r.PathValue(api.ParamToken)),
            Action:  api.CallbackAction(r.PathValue(api.ParamAction)),
            Request: r,
        })
        if err != nil {
            http.Error(w, err.Error(), status)
            return
        }
        w.WriteHeader(status)
    },
)

builtins.Callback records the outcome and returns the status to answer with, accepting duplicate callbacks. When you pass your own Handlers, give the same callback builder to builtins.All so its Service handler uses it.