Maestros (Controllers)
Overview
In X-Go, controllers are called Maes (short for Maestro). A Mae orchestrates a single business operation by executing a defined sequence of steps. Each Mae handles one coherent action — creating a resource, listing records, editing an entity, or performing a complex workflow.
Maes are protocol-agnostic. They receive a MaeIn input struct (free of HTTP concerns) and return a MaeOut output struct, both defined in src/dat/maes/. Routes call the Mae and then render the output in whatever protocol is required (HTML, JSON, etc.).
Location: src/mae/
Naming Conventions
Mae names follow the pattern {Verb}{Entity}Mae. The six standard CRUD verbs always require an entity name:
| Verb | R/W | Purpose | Example |
|---|---|---|---|
New |
Read | Display an empty creation form | NewGardenMae |
Create |
Write | Submit a creation form and persist the entity | CreateGardenMae |
Edit |
Read | Display a pre-filled update form | EditGardenTaskMae |
Update |
Write | Submit an update form and persist changes | UpdateGardenTaskMae |
Delete |
Write | Remove an entity | DeleteGardenMae |
List |
Read | Retrieve a paginated or filtered collection | ListPlantsMae |
New/Create and Edit/Update are read/write pairs: the first renders the form, the second handles the submission.
For non-CRUD operations, the pattern becomes {Verb}{Subject}Mae where the subject is not necessarily an entity. These verbs are optional and used as needed:
| Verb | R/W | Purpose | Example |
|---|---|---|---|
Overview |
Read | Show a summary or dashboard view | OverviewTodayTasksMae |
Display |
Read | Show details for a single entity | DisplayGardenMae |
See |
Read | Show a focused or contextual view of an entity | SeeGardenHealthMae |
Perform |
Write | Execute a complex multi-step operation | PerformSendingLettersMae |
See and Perform can be used as a read/write pair when a contextual view precedes a complex action.
MaeIn and MaeOut Structures
Every Mae has a pair of data structs defined in src/dat/maes/. These are the explicit input/output contracts.
Location: src/dat/maes/
MaeIn
// src/dat/maes/create_garden_mae.go
package maes
// |@@| C
import (
"my-app/src/dat/forms"
"my-app/src/lib/in_context"
)
type CreateGardenMaeIn struct {
Request CreateGardenMaeInReq
Context in_context.InContext
}
type CreateGardenMaeInReq struct {
Form forms.CreateGardenForm `form:"form"`
}
Context carries request metadata (authenticated user identity, session data) populated by the route before calling the Mae.
Request holds the raw input bound from the HTTP request — form values, query parameters, or JSON body fields.
MaeOut
type CreateGardenMaeOut struct {
In *CreateGardenMaeIn
Response CreateGardenMaeOutRes
Success bool
Ctx context.Context
Extra out_extra.OutExtra
}
type CreateGardenMaeOutRes struct {
Garden *mdl.Garden
FormBag *form.FormBag
Form forms.CreateGardenForm
FormFields []*presenter.FormField
}
Success— set totrueby the Mae only after all steps complete successfully.Extra— holds user-facing messages (success/failure notifications) added during step execution.Response— contains all data the view or JSON renderer needs.
List Mae Pattern
List Maes include pagination and filter support in the request:
type ListGardensMaeIn struct {
Request ListGardensMaeInReq
Context in_context.InContext
}
type ListGardensMaeInReq struct {
Page int `form:"page" json:"page"`
Filters []fetcher.RawFilter `form:"filters"`
Columns []presenter.Column `form:"columns"`
}
type ListGardensMaeOutRes struct {
Gardens []*mdl.Garden
GardensPagination *fetcher.FetcherPagination
List struct {
Docs []*mdl.Garden
Columns []*presenter.Column
}
}
Delete Mae Pattern
Delete Maes only need an identifier in the request:
type DeleteGardenMaeIn struct {
Request DeleteGardenMaeInReq
Context in_context.InContext
}
type DeleteGardenMaeInReq struct {
Id string `form:"id" json:"id"`
}
type DeleteGardenMaeOut struct {
In *DeleteGardenMaeIn
Response DeleteGardenMaeOutRes
Success bool
Ctx context.Context
Extra out_extra.OutExtra
}
type DeleteGardenMaeOutRes struct{}
Mae Implementation
The Mae struct lives in src/mae/ and wires together its steps and dependencies via injection. It exposes a single Act method.
// src/mae/create_garden_mae.go
package mae
// |@@| C
import (
"context"
"my-app/src/dat/maes"
"my-app/src/lib/maestro"
"my-app/src/lib/sql_db"
steps "my-app/src/mae/create_garden"
"my-app/src/srv/form_validators"
"my-app/src/srv/handlers"
"my-app/src/srv/molders"
)
type CreateGardenMae struct {
ConfigureFormStep *steps.ConfigureFormStep
CreateGardenFormMolder *molders.CreateGardenFormMolder
CreateGardenFormValidator *form_validators.CreateGardenFormValidator
GardenHandler *handlers.GardenHandler
Maestro *maestro.Maestro
RecordEventStep *steps.RecordEventStep
SaveEntityStep *steps.SaveEntityStep
SqlDb *sql_db.SqlDb
ValidateFormStep *steps.ValidateFormStep
}
func (this *CreateGardenMae) Act(ctx context.Context, in *maes.CreateGardenMaeIn) (*maes.CreateGardenMaeOut, error) {
out := &maes.CreateGardenMaeOut{In: in, Ctx: ctx}
acts := []func(ctx context.Context, in *maes.CreateGardenMaeIn, out *maes.CreateGardenMaeOut) (bool, error){
this.ConfigureFormStep.Act,
this.ValidateFormStep.Act,
this.SaveEntityStep.Act,
this.RecordEventStep.Act,
}
out, err := maestro.ActOnActs(ctx, in, out, acts)
if err != nil {
return out, err
}
out.Success = true
return out, nil
}
maestro.ActOnActs iterates the steps in order. If any step returns false or an error, execution stops immediately.
Steps
Each Mae’s steps are organized in a dedicated sub-package under src/mae/{action}_{entity}/. Steps are small, focused structs implementing a single part of the operation.
Location: src/mae/{action}_{entity}/
ConfigureFormStep
Prepares the response form and form fields for display. Runs on every request, including the initial GET before any data is submitted.
// src/mae/create_garden/configure_form_step.go
package steps
// |@@| F
import (
"context"
"my-app/src/dat/maes"
"my-app/src/lib/maestro"
"my-app/src/srv/form_presenters"
)
type ConfigureFormStep struct {
CreateGardenFormPresenter *form_presenters.CreateGardenFormPresenter
Maestro *maestro.Maestro
}
func (this *ConfigureFormStep) Act(ctx context.Context, in *maes.CreateGardenMaeIn, out *maes.CreateGardenMaeOut) (bool, error) {
out.Response.Form = in.Request.Form
out.Response.FormBag = this.Maestro.EmptyFormBag()
out.Response.FormFields = this.CreateGardenFormPresenter.Setup(&out.Response.Form)
return true, nil
}
ValidateFormStep
Runs form validation and adds a failure message if the form is invalid. Returning false halts execution — the route will re-render the form with validation errors.
// src/mae/create_garden/validate_form_step.go
package steps
// |@@| F
import (
"context"
"my-app/src/dat/maes"
"my-app/src/lib/maestro"
"my-app/src/srv/form_validators"
)
type ValidateFormStep struct {
CreateGardenFormValidator *form_validators.CreateGardenFormValidator
Maestro *maestro.Maestro
}
func (this *ValidateFormStep) Act(ctx context.Context, in *maes.CreateGardenMaeIn, out *maes.CreateGardenMaeOut) (bool, error) {
bag, err := this.CreateGardenFormValidator.Validate(ctx, &in.Request.Form)
out.Response.FormBag = bag
if err != nil {
this.Maestro.AddFailureMessage(&out.Extra, "Cannot submit form")
return false, err
}
return true, nil
}
SaveEntityStep
Converts the form into an entity using the Molder, persists it via the Handler, and adds a success message.
// src/mae/create_garden/save_entity_step.go
package steps
// |@@| W
import (
"context"
"my-app/src/dat/maes"
"my-app/src/lib/maestro"
"my-app/src/mdl"
"my-app/src/srv/handlers"
"my-app/src/srv/molders"
)
type SaveEntityStep struct {
CreateGardenFormMolder *molders.CreateGardenFormMolder
GardenHandler *handlers.GardenHandler
Maestro *maestro.Maestro
}
func (this *SaveEntityStep) Act(ctx context.Context, in *maes.CreateGardenMaeIn, out *maes.CreateGardenMaeOut) (bool, error) {
var err error
entity := &mdl.Garden{}
entity, _ = this.CreateGardenFormMolder.ToEntity(&in.Request.Form, entity, in.Context.RequestPresence)
out.Response.Garden, err = this.GardenHandler.Create(ctx, entity, nil)
if err != nil {
return false, err
}
this.Maestro.AddSuccessMessage(&out.Extra, "Garden created")
return true, nil
}
RecordEventStep
Appends an audit event after a successful write operation.
// src/mae/create_garden/record_event_step.go
package steps
// |@@| F
import (
"context"
"my-app/src/dat/maes"
"my-app/src/lib/maestro"
"my-app/src/srv/handlers"
)
type RecordEventStep struct {
EventHandler *handlers.EventHandler
Maestro *maestro.Maestro
}
func (this *RecordEventStep) Act(ctx context.Context, in *maes.CreateGardenMaeIn, out *maes.CreateGardenMaeOut) (bool, error) {
event := this.Maestro.NewEvent(ctx)
event.What = "create_garden"
event.ResourceId = out.Response.Garden.Id
event.ResourceType = "garden"
this.EventHandler.MustCreate(ctx, event, nil)
return true, nil
}
DeleteEntityStep
Fetches and removes an entity by ID.
// src/mae/delete_garden/delete_entity_step.go
package steps
import (
"context"
"my-app/src/dat/maes"
"my-app/src/lib/maestro"
"my-app/src/srv/fetchers"
"my-app/src/srv/handlers"
)
type DeleteEntityStep struct {
GardenFetcher *fetchers.GardenFetcher
GardenHandler *handlers.GardenHandler
Maestro *maestro.Maestro
}
func (this *DeleteEntityStep) Act(ctx context.Context, in *maes.DeleteGardenMaeIn, out *maes.DeleteGardenMaeOut) (bool, error) {
garden, exists, err := this.GardenFetcher.FindOneById(ctx, in.Request.Id, nil)
if err != nil {
return false, err
}
if !exists {
return false, nil
}
err = this.GardenHandler.Delete(ctx, garden, nil)
if err != nil {
return false, err
}
this.Maestro.AddSuccessMessage(&out.Extra, "Garden deleted")
return true, nil
}
Step Control Flow
Steps return (bool, error):
| Return | Meaning |
|---|---|
true, nil |
Step succeeded — continue to the next step |
false, nil |
Step decided to stop — execution halts, no error is propagated |
false, err |
Step failed — execution halts, error is propagated to the route |
Use the maestro helpers for common returns:
return maestro.Continue() // (true, nil)
return this.Maestro.Stop() // (false, nil)
return maestro.Unauthorized() // (false, error "Not authorized")
User Feedback via OutExtra
Maes communicate results to the user through out.Extra.Messages. The Maestro service provides helpers:
this.Maestro.AddSuccessMessage(&out.Extra, "Garden saved")
this.Maestro.AddFailureMessage(&out.Extra, "Cannot save garden")
The route reads out.Extra and renders these messages as flash notifications.
Modifying Maes
Why Modify a Mae
Maes evolve alongside the application’s business rules. Common reasons to touch an existing Mae:
- New side effects — send a notification, trigger a follow-up action, or update a related entity after a write
- Authorization — restrict an action to certain users before any business logic runs
- Data enrichment — populate extra fields in the response for a view that now needs them
- Changed validation — tighten or relax the rules applied before a save
- Audit and observability — record more context in the event log
When to Modify a Mae vs. When to Create a New Mae Step
Modify an existing Mae when the operation is the same but its behavior needs to change or grow. Create a new Mae step when you need a distinct operation — a different action, a different entity, or a meaningfully different flow.
Within an existing Mae, add a new step when the new logic is substantial or testable on its own. Change an existing step when the modification is small and tightly coupled to that step’s existing purpose.
How to Create a New Mae
Creating a new Mae involves three layers: the data contract, the Mae itself, and its steps.
1. Define MaeIn and MaeOut in src/dat/maes/
Create a new file named after your Mae. Define what the route will pass in and what the Mae will return.
// src/dat/maes/publish_garden_mae.go
package maes
// |@@| C
import (
"my-app/src/dat/forms"
"my-app/src/lib/in_context"
)
type PublishGardenMaeIn struct {
Request PublishGardenMaeInReq
Context in_context.InContext
}
type PublishGardenMaeInReq struct {
Id string `form:"id" json:"id"`
Form forms.PublishGardenForm `form:"form"`
}
type PublishGardenMaeOut struct {
In *PublishGardenMaeIn
Response PublishGardenMaeOutRes
Success bool
Ctx context.Context
Extra out_extra.OutExtra
}
type PublishGardenMaeOutRes struct {
Garden *mdl.Garden
FormBag *form.FormBag
Form forms.PublishGardenForm
FormFields []*presenter.FormField
}
2. Create the Mae struct in src/mae/
Create src/mae/publish_garden_mae.go. Declare all dependencies as exported fields and implement Act by listing the steps in execution order.
// src/mae/publish_garden_mae.go
package mae
// |@@| C
import (
"context"
"my-app/src/dat/maes"
"my-app/src/lib/maestro"
steps "my-app/src/mae/publish_garden"
"my-app/src/srv/form_validators"
"my-app/src/srv/handlers"
"my-app/src/srv/molders"
)
type PublishGardenMae struct {
ConfigureFormStep *steps.ConfigureFormStep
PublishGardenFormMolder *molders.PublishGardenFormMolder
PublishGardenFormValidator *form_validators.PublishGardenFormValidator
GardenHandler *handlers.GardenHandler
Maestro *maestro.Maestro
RecordEventStep *steps.RecordEventStep
PublishEntityStep *steps.PublishEntityStep
ValidateFormStep *steps.ValidateFormStep
}
func (this *PublishGardenMae) Act(ctx context.Context, in *maes.PublishGardenMaeIn) (*maes.PublishGardenMaeOut, error) {
out := &maes.PublishGardenMaeOut{In: in, Ctx: ctx}
acts := []func(ctx context.Context, in *maes.PublishGardenMaeIn, out *maes.PublishGardenMaeOut) (bool, error){
this.ConfigureFormStep.Act,
this.ValidateFormStep.Act,
this.PublishEntityStep.Act,
this.RecordEventStep.Act,
}
out, err := maestro.ActOnActs(ctx, in, out, acts)
if err != nil {
return out, err
}
out.Success = true
return out, nil
}
3. Create the step sub-package in src/mae/publish_garden/
Each step lives in its own file within the sub-package. Start with the steps your Mae needs and add more as the operation grows.
// src/mae/publish_garden/publish_entity_step.go
package steps
// |@@| W
import (
"context"
"my-app/src/dat/maes"
"my-app/src/lib/maestro"
"my-app/src/srv/fetchers"
"my-app/src/srv/handlers"
)
type PublishEntityStep struct {
GardenFetcher *fetchers.GardenFetcher
GardenHandler *handlers.GardenHandler
Maestro *maestro.Maestro
}
func (this *PublishEntityStep) Act(ctx context.Context, in *maes.PublishGardenMaeIn, out *maes.PublishGardenMaeOut) (bool, error) {
garden, exists, err := this.GardenFetcher.FindOneById(ctx, in.Request.Id, nil)
if err != nil {
return false, err
}
if !exists {
return this.Maestro.Stop()
}
out.Response.Garden, err = this.GardenHandler.Publish(ctx, garden, nil)
if err != nil {
return false, err
}
this.Maestro.AddSuccessMessage(&out.Extra, "Garden published")
return maestro.Continue()
}
4. Wire it up
Register the new Mae and its steps in the dependency injection container so the router can resolve them. Follow the same registration pattern used by existing Maes in your application’s wiring layer.
File layout for the new Mae:
src/
├── dat/
│ └── maes/
│ └── publish_garden_mae.go # MaeIn / MaeOut structs
├── mae/
│ ├── publish_garden_mae.go # Mae struct and Act method
│ └── publish_garden/
│ ├── configure_form_step.go # Prepare form fields
│ ├── validate_form_step.go # Validate form input
│ ├── publish_entity_step.go # Core business logic
│ └── record_event_step.go # Audit logging
🪪 For Blue-Lila Employees : How to Add a Step to an existing Mae
Add a new step in gen-x project’s depicter with
Depicter.AddStepToMae(ctx, name, data, position, maeName).This will create a new step service and attach it to the named Maestro at the given position.
How to Extend MaeIn or MaeOut
When a step needs to pass or receive data that isn’t currently in the contract, extend the relevant struct in src/dat/maes/.
Adding a request field (e.g. a flag passed from the route):
type CreateGardenMaeInReq struct {
Form forms.CreateGardenForm `form:"form"`
SendWelcome bool `form:"send_welcome" json:"send_welcome"` // added
}
Adding a response field (e.g. a count the view now needs):
type CreateGardenMaeOutRes struct {
Garden *mdl.Garden
FormBag *form.FormBag
Form forms.CreateGardenForm
FormFields []*presenter.FormField
TotalGardens int // added — populated by a step
}
Steps read from in.Request and write to out.Response, so any field added here is immediately accessible to all steps in the Mae.
Ordering Guidelines
When inserting a new step, use this order as a reference:
- Authorization — gate access before anything else
- ConfigureForm — always prepare the form, including on POST so the view can re-render on failure
- ValidateForm — stop early if input is invalid
- SaveEntity / DeleteEntity — write to the database
- Side effects — notifications, follow-up actions, cache invalidation
- RecordEvent — audit trail last, so the event is only written on full success
Mae File Structure Summary
For a CreateGarden controller, the complete file layout is:
src/
├── dat/
│ └── maes/
│ └── create_garden_mae.go # MaeIn / MaeOut structs
├── mae/
│ ├── create_garden_mae.go # Mae struct and Act method
│ └── create_garden/
│ ├── configure_form_step.go # Prepare form fields
│ ├── validate_form_step.go # Validate form input
│ ├── save_entity_step.go # Persist entity
│ └── record_event_step.go # Audit logging