-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Proposal: Structured Output via Action Metadata Schema
Summary
Extend action metadata (action.yaml) with a result schema, symmetric with existing arguments. This enables --json CLI output, web UI rendering, and API responses - all driven by declarative schema.
Motivation
Current State
Actions define input via arguments in action.yaml:
action:
title: Add
arguments:
- name: package
type: string
- name: type
type: string
enum: [git, http]
default: gitBut output is undefined - actions print directly to stdout with no contract.
Problem
- No machine-readable output - scripts parse human text
- Web UI can't render results - no schema to bind components
- Inconsistent formats - each action formats differently
- No documentation - output structure is implicit
Proposal
Add result section to action metadata using JSON Schema syntax (same as arguments):
runtime: plugin
action:
title: Show
description: Show package dependency details
arguments:
- name: package
title: Package
description: Package name (optional, shows all if omitted)
type: string
result:
type: object
properties:
packages:
type: array
description: List of package dependencies
items:
type: object
properties:
name:
type: string
description: Package name
ref:
type: string
description: Git reference (branch, tag, commit)
url:
type: string
description: Source URL
type:
type: string
enum: [git, http]
components:
type: array
description: Components provided by this package
items:
type: string
summary:
type: object
properties:
total_packages:
type: integer
total_components:
type: integerDesign
Schema Location
In action.yaml alongside arguments:
action:
title: ...
description: ...
arguments: [...] # Input schema (existing)
result: {...} # Output schema (new)Schema Format
Standard JSON Schema subset (matching current arguments capabilities):
| Field | Type | Description |
|---|---|---|
type |
string | object, array, string, integer, boolean, number |
properties |
object | For type: object - nested property definitions |
items |
object | For type: array - item schema |
enum |
array | Allowed values |
description |
string | Human-readable description |
required |
array | Required property names |
Global Flag
plasmactl [action] --json # Output result as JSON
plasmactl [action] --yaml # Output result as YAML (optional)Behavior
| Scenario | Result |
|---|---|
--json + has result schema |
JSON to stdout, exit 0 |
--json + no result schema |
Error: "action does not support structured output" |
No flag + has result schema |
Human-readable output (action controls format) |
No flag + no result schema |
Human-readable output (unchanged behavior) |
Stream Separation
stdout → Structured result (JSON/YAML) when --json/--yaml
stdout → Human-readable when no flag
stderr → Progress, debug, warnings, errors (always)
Error Handling
On error with --json:
{
"error": {
"message": "compose.yaml not found",
"code": "COMPOSE_NOT_FOUND"
}
}Exit code remains non-zero. Error schema is standard across all actions.
Implementation
Phase 1: Launchr Core
- Parse
resultfield from action.yaml - Add
--jsonglobal flag - Add result serialization in action runner
- Terminal suppresses decorations in JSON mode
// pkg/action/action.go
type ActionConfig struct {
Title string
Description string
Arguments []Argument
Result *jsonschema.Schema // NEW
}
// pkg/action/runner.go
func (r *Runner) Run(action Action) error {
err := action.Execute()
if r.jsonMode && r.actionConfig.Result != nil {
result := action.Result()
// Optionally validate against schema
return json.NewEncoder(os.Stdout).Encode(result)
}
return err
}Phase 2: Action Interface
Actions implement optional Result() method:
// Actions that support structured output
type ResultProvider interface {
Result() any
}
// Example implementation
func (s *Show) Result() any {
return s.result // Built during Execute()
}Phase 3: Migrate Actions
Add result schema to action.yaml files:
# model:show
result:
type: object
properties:
packages:
type: array
items:
$ref: "#/definitions/package"
definitions:
package:
type: object
properties:
name: { type: string }
ref: { type: string }
components: { type: array, items: { type: string } }
# component:list
result:
type: array
items:
type: object
properties:
name: { type: string }
layer: { type: string }
kind: { type: string }
# node:list
result:
type: array
items:
type: object
properties:
name: { type: string }
chassis: { type: string }
status: { type: string }Web Interface Integration
The schema enables automatic UI generation:
// Schema-driven rendering
function ActionResult({ schema, data }: { schema: JSONSchema, data: any }) {
if (schema.type === 'array') {
return <Table schema={schema.items} data={data} />;
}
if (schema.type === 'object') {
return <ObjectView schema={schema} data={data} />;
}
return <Primitive value={data} />;
}
// Action form + result view
function ActionView({ action }: { action: ActionConfig }) {
const [result, setResult] = useState(null);
return (
<>
{/* Input form from arguments schema */}
<ActionForm schema={action.arguments} onSubmit={runAction} />
{/* Result view from result schema */}
{result && <ActionResult schema={action.result} data={result} />}
</>
);
}Custom components may hypothetically be specified via schema extension:
result:
type: array
x-component: "tree-view" # Hint for web UI
items:
type: object
properties:
name: { type: string }
children: { type: array, items: { $ref: "#" } }Examples
CLI Usage
# Human-readable (default)
$ plasmactl model:show
Package: plasma-core
ref: prepare
url: https://projects.skilld.cloud/skilld/pla-plasma.git
Components (15):
interaction.applications.dashboards
...
# JSON for scripting
$ plasmactl model:show --json
{
"packages": [
{
"name": "plasma-core",
"ref": "prepare",
"url": "https://projects.skilld.cloud/skilld/pla-plasma.git",
"components": ["interaction.applications.dashboards", ...]
}
]
}
# Piping to jq
$ plasmactl model:show --json | jq '.packages[].name'
"plasma-core"
"plasma-work"
# Component filtering
$ plasmactl component:list --json | jq '.[] | select(.layer == "interaction")'Action Implementation
// show.go
type Show struct {
action.WithLogger
action.WithTerm
Package string
result *ShowResult
}
type ShowResult struct {
Packages []PackageInfo `json:"packages"`
Summary *Summary `json:"summary,omitempty"`
}
func (s *Show) Execute() error {
// Build result
s.result = &ShowResult{}
for _, dep := range cfg.Dependencies {
pkg := PackageInfo{
Name: dep.Name,
Ref: dep.Source.Ref,
URL: dep.Source.URL,
}
s.result.Packages = append(s.result.Packages, pkg)
// Human output (skipped in JSON mode)
if !s.Term().JSONMode() {
fmt.Printf("Package: %s\n", dep.Name)
fmt.Printf(" ref: %s\n", dep.Source.Ref)
}
}
return nil
}
// Implement ResultProvider
func (s *Show) Result() any {
return s.result
}Migration Path
- Launchr: Add
resultparsing +--jsonflag - Core actions: Add schema to
*:list,*:show,*:query - Documentation: Auto-generate from schema
- Web UI: Schema-driven result rendering
- Validation: Optional result validation against schema
Backward Compatibility
- Actions without
resultwork unchanged --jsonon legacy actions returns clear error- Human-readable output unaffected
- No changes to existing action signatures
Benefits
| Feature | Input (arguments) | Output (result) |
|---|---|---|
| CLI validation | ✅ Existing | ✅ Optional |
| Web form generation | ✅ Existing | ✅ New |
| Documentation | ✅ Existing | ✅ New |
| Type safety | ✅ Go binding | ✅ Go binding |
| JSON output | N/A | ✅ --json flag |
| YAML output | N/A | ✅ --yaml flag (optional) |
Open Questions
- Schema validation: Validate result against schema in dev mode?
- Streaming: Large results - JSON Lines format?
- References: Support
$reffor shared definitions? - Pagination: Standard pagination schema for large lists?