Advanced

Module Development

Build a Go module that compiles directly into Shiro. Modules implement a simple two-method interface and are registered via shiro add module.

Prerequisites

  • • Go 1.21+
  • shiro binary installed
  • • GitHub repo for publishing (or local path for development)
1

Create the Go module

bash
mkdir shiro-mymodule && cd shiro-mymodule
go mod init github.com/your-org/shiro-mymodule
2

Implement the Module interface

Create mymodule/mymodule.go. Your struct must implement both Run and Metadata.

go
package mymodule

import (
    "context"
    "fmt"
    "reflect"
)

// SchemaField mirrors modules.SchemaField — copy this to avoid importing
// shiro internal packages directly.
type SchemaField struct {
    Type        string      `json:"type"`
    Description string      `json:"description"`
    Required    bool        `json:"required"`
    Default     interface{} `json:"default,omitempty"`
}

type ModuleMetadata struct {
    Name         string                 `json:"name"`
    Description  string                 `json:"description"`
    InputSchema  map[string]SchemaField `json:"input_schema"`
    OutputSchema map[string]SchemaField `json:"output_schema"`
}

type MyModule struct{}

func NewMyModule() *MyModule { return &MyModule{} }

// Run is called by Shiro for each step. Extract config from the step,
// perform your logic, and return output as map[string]interface{}.
func (m *MyModule) Run(ctx context.Context, stepCtx interface{}, step interface{}) (map[string]interface{}, error) {
    cfg, err := extractConfig(step)
    if err != nil {
        return nil, err
    }

    input, _ := cfg["input"].(string)
    if input == "" {
        return nil, fmt.Errorf("input is required")
    }

    // ... your logic here ...
    result := "processed: " + input

    return map[string]interface{}{
        "result":  result,
        "success": true,
    }, nil
}

func (m *MyModule) Metadata() ModuleMetadata {
    return ModuleMetadata{
        Name:        "mymodule",
        Description: "Example custom module",
        InputSchema: map[string]SchemaField{
            "input": {Type: "string", Description: "Input value", Required: true},
        },
        OutputSchema: map[string]SchemaField{
            "result":  {Type: "string", Description: "Processed output"},
            "success": {Type: "boolean", Description: "Success flag"},
        },
    }
}

// extractConfig pulls Config from the Shiro workflow.Step via reflection.
// This avoids importing shiro's internal/workflow package.
func extractConfig(step interface{}) (map[string]interface{}, error) {
    if step == nil {
        return map[string]interface{}{}, nil
    }
    v := reflect.ValueOf(step)
    if v.Kind() == reflect.Ptr {
        if v.IsNil() {
            return map[string]interface{}{}, nil
        }
        v = v.Elem()
    }
    if v.Kind() == reflect.Struct {
        f := v.FieldByName("Config")
        if f.IsValid() && !f.IsNil() {
            if cfg, ok := f.Interface().(map[string]interface{}); ok {
                return cfg, nil
            }
        }
        return map[string]interface{}{}, nil
    }
    return nil, fmt.Errorf("unexpected step type %T", step)
}
3

Add a module.yaml

module.yaml
name: mymodule
type: builtin
description: "Example custom module"
version: 1.0.0
source: https://github.com/your-org/shiro-mymodule
factory: NewMyModule
4

Push to GitHub and register

bash
git push origin main

# In your project directory:
shiro add module github.com/your-org/shiro-mymodule

This adds an entry to .shiro/modules/registry.yaml.

5

Rebuild Shiro

bash
# Must be run from the shiro-automation source directory
shiro build

shiro build code-generates the registry import, runs go mod tidy, and recompiles the binary. After this, mymodule is available as a step type in all workflows.

6

Use in a workflow

json
{
  "id": "my_step",
  "type": "mymodule",
  "config": {
    "input": "hello from shiro"
  }
}

Tips

Use reflection for config extraction
Copy the extractConfig helper from an existing module (e.g. jira-datacenter). Never import shiro internal packages — this creates a circular dependency.
Return structured output
The map returned by Run() is available to downstream steps as {{steps.<id>.output.*}}. Use consistent key names.
Handle context cancellation
Check ctx.Done() in long-running operations. Shiro may cancel the context on timeout or user interrupt.
Test locally before building
Write unit tests for Run() directly. You can mock the step using a simple struct with a Config field.
📋
Full API reference

See the API Contract for the complete Module interface, schema types, and subprocess/HTTP protocol specs.