semaphoreui/semaphore · error

invalid operation

Error message

invalid operation

What it means

ErrInvalidOperation is the sentinel error ('invalid operation') returned by store/service methods when an operation is not permitted in the current state — e.g. deleting an environment that is still referenced by templates, or deleting integrations/matchers that are in use. The API layer maps it to HTTP 409 Conflict via WriteError, or a 400 with a domain message.

Solutions

  1. Delete the dependent resources first (e.g. remove templates referencing the environment) and retry
  2. Check the sentinel with errors.Is(err, db.ErrInvalidOperation) to surface a 409/400 with a helpful message instead of a generic 500
  3. Query the references (templates using the environment, integration usages) before attempting deletion to warn users up front

Example fix

// before
if err := c.environmentService.Delete(env.ProjectID, env.ID); err != nil {
    helpers.WriteError(w, err, http.StatusInternalServerError)
}
// after
if err := c.environmentService.Delete(env.ProjectID, env.ID); err != nil {
    if errors.Is(err, db.ErrInvalidOperation) {
        helpers.WriteJSON(w, http.StatusBadRequest, map[string]any{"error": "Environment is in use by one or more templates"})
        return
    }
    helpers.WriteError(w, err, http.StatusInternalServerError)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before deleting, check references
templates, err := templateService.GetAll(env.ProjectID)
if err == nil {
    for _, t := range templates {
        if usesEnvironment(t, env.ID) {
            return errors.New("environment is in use by templates")
        }
    }
}

Try / catch

err := c.environmentService.Delete(env.ProjectID, env.ID)
if errors.Is(err, db.ErrInvalidOperation) {
    helpers.WriteJSON(w, http.StatusBadRequest, map[string]any{"error": "Environment is in use by one or more templates"})
    return
}
if err != nil {
    helpers.WriteError(w, err.Error(), http.StatusInternalServerError)
}

Prevention

When it happens

Trigger: environmentService.Delete for an environment used by one or more templates (api/projects/environment.go:299); DeleteIntegration, DeleteIntegrationExtractValue, DeleteIntegrationMatcher, or RemoveInventory on entities that are in use.

Common situations: Users trying to remove an environment still referenced by project templates; deleting an integration whose extracted values/matchers are wired into resources; cascading cleanup scripts hitting in-use entities.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/0670fc28257cffa1. Report an issue: GitHub.

Appendix: source

Thrown at db/Store.go:159

// ObjectProps describe database entities.
// It mainly used for NoSQL implementations (currently BoltDB) to preserve same
// data structure of different implementations and easy change it if required.
type ObjectProps struct {
	TableName             string
	Type                  reflect.Type // to which type the table bust be mapped.
	IsGlobal              bool         // doesn't belong to other table, for example to project or user.
	ReferringColumnSuffix string
	PrimaryColumnName     string
	SortableColumns       []string
	DefaultSortingColumn  string
	SortInverted          bool // sort from high to low object ID by default. It is useful for some NoSQL implementations.
	Ownerships            []*ObjectProps
	SelectColumns         []string
}

var ErrNotFound = errors.New("no rows in result set")
var ErrInvalidOperation = errors.New("invalid operation")

type TaskStatUnit string

const TaskStatUnitDay TaskStatUnit = "day"
const TaskStatUnitWeek TaskStatUnit = "week"
const TaskStatUnitMonth TaskStatUnit = "month"

type TaskFilter struct {
	Start  *time.Time `json:"start"`
	End    *time.Time `json:"end"`
	UserID *int       `json:"user_id"`
	Status []task_logger.TaskStatus
}

type TaskStat struct {
	Date          string                         `json:"date"`
	CountByStatus map[task_logger.TaskStatus]int `json:"count_by_status"`
	AvgDuration   int                            `json:"avg_duration"`

View on GitHub (pinned to 1774ccb71a)