Documentation Index
Fetch the complete documentation index at: https://docs.osmedeus.org/llms.txt
Use this file to discover all available pages before exploring further.
Add custom step executors to extend workflow capabilities.
Overview
Step types are handlers that execute specific step configurations. Each type has a dedicated executor in internal/executor/.
Steps to Add a New Step Type
1. Define the Type Constant
Add to internal/core/types.go:
// StepType represents the type of step
type StepType string
const (
StepTypeBash StepType = "bash"
StepTypeFunction StepType = "function"
StepTypeForeach StepType = "foreach"
StepTypeParallelSteps StepType = "parallel-steps"
StepTypeRemoteBash StepType = "remote-bash"
StepTypeHTTP StepType = "http"
StepTypeLLM StepType = "llm"
StepTypeMyNew StepType = "mynew" // Add your new type
)
2. Add Step Fields (if needed)
Add fields to internal/core/step.go:
type Step struct {
// Existing fields...
// New fields for your step type
MyNewField string `yaml:"my_new_field,omitempty"`
MyNewConfig *MyNewConfig `yaml:"my_new_config,omitempty"`
}
type MyNewConfig struct {
Option1 string `yaml:"option1,omitempty"`
Option2 int `yaml:"option2,omitempty"`
}
3. Create the Executor
Create internal/executor/mynew_executor.go:
package executor
import (
"context"
"github.com/osmedeus/osmedeus-ng/internal/core"
"github.com/osmedeus/osmedeus-ng/internal/template"
)
type MyNewExecutor struct {
templateEngine *template.Engine
}
func NewMyNewExecutor(templateEngine *template.Engine) *MyNewExecutor {
return &MyNewExecutor{
templateEngine: templateEngine,
}
}
func (e *MyNewExecutor) Execute(
ctx context.Context,
step *core.Step,
execCtx *core.ExecutionContext,
) (*core.StepResult, error) {
// 1. Render templates in step fields
renderedField, err := e.templateEngine.Render(step.MyNewField, execCtx.Variables)
if err != nil {
return nil, fmt.Errorf("template render failed: %w", err)
}
// 2. Perform your step logic
output, err := e.doSomething(ctx, renderedField, step.MyNewConfig)
if err != nil {
return &core.StepResult{
Success: false,
Error: err,
}, nil
}
// 3. Return result
return &core.StepResult{
Success: true,
Output: output,
}, nil
}
func (e *MyNewExecutor) doSomething(
ctx context.Context,
field string,
config *core.MyNewConfig,
) (string, error) {
// Implementation here
return "result", nil
}
4. Register in Dispatcher
Update internal/executor/dispatcher.go:
type StepDispatcher struct {
bashExecutor *BashExecutor
functionExecutor *FunctionExecutor
foreachExecutor *ForeachExecutor
parallelExecutor *ParallelExecutor
remoteBashExecutor *RemoteBashExecutor
httpExecutor *HTTPExecutor
llmExecutor *LLMExecutor
myNewExecutor *MyNewExecutor // Add your executor
runner runner.Runner
}
func NewStepDispatcher(
templateEngine *template.Engine,
functionRegistry *functions.Registry,
runner runner.Runner,
) *StepDispatcher {
return &StepDispatcher{
// Existing executors...
myNewExecutor: NewMyNewExecutor(templateEngine),
runner: runner,
}
}
func (d *StepDispatcher) Dispatch(
ctx context.Context,
step *core.Step,
execCtx *core.ExecutionContext,
) (*core.StepResult, error) {
switch step.Type {
case core.StepTypeBash:
return d.bashExecutor.Execute(ctx, step, execCtx, d.runner)
// ... other cases ...
case core.StepTypeMyNew:
return d.myNewExecutor.Execute(ctx, step, execCtx)
default:
return nil, fmt.Errorf("unknown step type: %s", step.Type)
}
}
5. Add Validation
Update internal/parser/validator.go:
func (v *Validator) validateStep(step *core.Step) error {
switch step.Type {
// ... existing cases ...
case core.StepTypeMyNew:
return v.validateMyNewStep(step)
}
return nil
}
func (v *Validator) validateMyNewStep(step *core.Step) error {
if step.MyNewField == "" {
return fmt.Errorf("step '%s': my_new_field is required for mynew step type", step.Name)
}
return nil
}
6. Write Tests
Create internal/executor/mynew_executor_test.go:
package executor
import (
"context"
"testing"
"github.com/osmedeus/osmedeus-ng/internal/core"
"github.com/osmedeus/osmedeus-ng/internal/template"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMyNewExecutor_Execute(t *testing.T) {
engine := template.NewEngine()
executor := NewMyNewExecutor(engine)
step := &core.Step{
Name: "test-step",
Type: core.StepTypeMyNew,
MyNewField: "test-value",
}
execCtx := &core.ExecutionContext{
Variables: map[string]interface{}{
"target": "example.com",
},
}
result, err := executor.Execute(context.Background(), step, execCtx)
require.NoError(t, err)
assert.True(t, result.Success)
assert.NotEmpty(t, result.Output)
}
Example: Custom Notification Step
// internal/executor/notify_executor.go
type NotifyExecutor struct {
templateEngine *template.Engine
}
func (e *NotifyExecutor) Execute(
ctx context.Context,
step *core.Step,
execCtx *core.ExecutionContext,
) (*core.StepResult, error) {
// Render message template
message, err := e.templateEngine.Render(step.NotifyMessage, execCtx.Variables)
if err != nil {
return nil, err
}
// Send notification based on channel
switch step.NotifyChannel {
case "slack":
err = e.sendSlack(ctx, step.NotifyConfig.WebhookURL, message)
case "discord":
err = e.sendDiscord(ctx, step.NotifyConfig.WebhookURL, message)
case "telegram":
err = e.sendTelegram(ctx, step.NotifyConfig, message)
}
if err != nil {
return &core.StepResult{Success: false, Error: err}, nil
}
return &core.StepResult{Success: true, Output: "Notification sent"}, nil
}
Usage in workflow:
- name: notify-complete
type: notify
notify_channel: slack
notify_message: "Scan completed for {{target}}"
notify_config:
webhook_url: "{{slack_webhook}}"
Best Practices
- Always render templates before using step fields
- Support context cancellation via
ctx.Done()
- Return meaningful errors with context
- Export useful values via StepResult
- Write comprehensive tests
- Add validation rules for required fields
Next Steps