Feat v0.3.6 demo workflows + interactive demo
Four example workflow packages proving the engine end-to-end: - Bug Report Triage: public entry, branch routing, SLA timer - Employee Onboarding: Starlark automated stages, signoff gate - Content Approval: multi-party signoff, revision cycle loop - Webhook Notifier: http.post, connections fallback, delivery logging Demo surface at /s/workflow-demo with cards, stage diagrams, Starlark viewer, API examples, active/published status, and copyable public links. Engine fixes: started_by in automated context, sla_seconds in package installer, parseSnapshotStages for wrapped/legacy formats. Platform fixes: extension SDK boot in base.html (Preact globals + boot()), admin teams paginated response extraction, workflow adoption endpoint (POST /teams/:teamId/workflows/:id/adopt), team-admin copyable link. Review pass remains on roadmap for remaining UI polish. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,10 +73,11 @@ func (e *Engine) processAutomatedStage(ctx context.Context, inst *models.Workflo
|
||||
}
|
||||
|
||||
// Build context dict for the hook
|
||||
ctxDict := starlark.NewDict(4)
|
||||
ctxDict := starlark.NewDict(5)
|
||||
_ = ctxDict.SetKey(starlark.String("instance_id"), starlark.String(inst.ID))
|
||||
_ = ctxDict.SetKey(starlark.String("current_stage"), starlark.String(inst.CurrentStage))
|
||||
_ = ctxDict.SetKey(starlark.String("workflow_id"), starlark.String(inst.WorkflowID))
|
||||
_ = ctxDict.SetKey(starlark.String("started_by"), starlark.String(inst.StartedBy))
|
||||
|
||||
// Parse stage_data into Starlark dict
|
||||
var dataMap map[string]interface{}
|
||||
|
||||
@@ -18,6 +18,25 @@ import (
|
||||
// MaxConsecutiveAutomated is the cycle guard limit for automated stages.
|
||||
const MaxConsecutiveAutomated = 10
|
||||
|
||||
// parseSnapshotStages handles both snapshot formats:
|
||||
// - Wrapped: {"stages": [...], "workflow": {...}} (from Publish handler)
|
||||
// - Legacy: [...] (from early tests)
|
||||
func parseSnapshotStages(raw json.RawMessage) ([]models.WorkflowStage, error) {
|
||||
// Try wrapped format first
|
||||
var wrapped struct {
|
||||
Stages []models.WorkflowStage `json:"stages"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &wrapped); err == nil && len(wrapped.Stages) > 0 {
|
||||
return wrapped.Stages, nil
|
||||
}
|
||||
// Fallback to flat array
|
||||
var stages []models.WorkflowStage
|
||||
if err := json.Unmarshal(raw, &stages); err != nil {
|
||||
return nil, fmt.Errorf("corrupt version snapshot: %w", err)
|
||||
}
|
||||
return stages, nil
|
||||
}
|
||||
|
||||
// Engine orchestrates workflow instance lifecycle.
|
||||
type Engine struct {
|
||||
stores store.Stores
|
||||
@@ -45,9 +64,9 @@ func (e *Engine) Start(ctx context.Context, workflowID string, initialData json.
|
||||
return nil, fmt.Errorf("no published version: %w", err)
|
||||
}
|
||||
|
||||
var stages []models.WorkflowStage
|
||||
if err := json.Unmarshal(ver.Snapshot, &stages); err != nil {
|
||||
return nil, fmt.Errorf("corrupt version snapshot: %w", err)
|
||||
stages, err := parseSnapshotStages(ver.Snapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(stages) == 0 {
|
||||
return nil, fmt.Errorf("workflow has no stages")
|
||||
@@ -121,9 +140,9 @@ func (e *Engine) advanceInternal(ctx context.Context, instanceID string, stageDa
|
||||
return nil, fmt.Errorf("version not found: %w", err)
|
||||
}
|
||||
|
||||
var stages []models.WorkflowStage
|
||||
if err := json.Unmarshal(ver.Snapshot, &stages); err != nil {
|
||||
return nil, fmt.Errorf("corrupt snapshot: %w", err)
|
||||
stages, err := parseSnapshotStages(ver.Snapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find current stage ordinal
|
||||
@@ -321,9 +340,9 @@ func (e *Engine) AdvancePublic(ctx context.Context, entryToken string, stageData
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("version not found: %w", err)
|
||||
}
|
||||
var stages []models.WorkflowStage
|
||||
if err := json.Unmarshal(ver.Snapshot, &stages); err != nil {
|
||||
return nil, fmt.Errorf("corrupt snapshot: %w", err)
|
||||
stages, err := parseSnapshotStages(ver.Snapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find current stage and validate audience
|
||||
@@ -356,9 +375,9 @@ func (e *Engine) SubmitSignoff(ctx context.Context, instanceID, userID, decision
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("version not found: %w", err)
|
||||
}
|
||||
var stages []models.WorkflowStage
|
||||
if err := json.Unmarshal(ver.Snapshot, &stages); err != nil {
|
||||
return nil, fmt.Errorf("corrupt snapshot: %w", err)
|
||||
stages, err := parseSnapshotStages(ver.Snapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find current stage
|
||||
@@ -421,8 +440,8 @@ func CheckClaimRole(ctx context.Context, stores store.Stores, assignment *models
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var stages []models.WorkflowStage
|
||||
if err := json.Unmarshal(ver.Snapshot, &stages); err != nil {
|
||||
stages, parseErr := parseSnapshotStages(ver.Snapshot)
|
||||
if parseErr != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -91,8 +91,8 @@ func (sc *Scanner) runScan() {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var stages []models.WorkflowStage
|
||||
if json.Unmarshal(v.Snapshot, &stages) != nil {
|
||||
stages, parseErr := parseSnapshotStages(v.Snapshot)
|
||||
if parseErr != nil {
|
||||
return nil
|
||||
}
|
||||
versionCache[key] = stages
|
||||
|
||||
Reference in New Issue
Block a user