Workflow
Gestalt workflows let you invoke an ordered set of app operations and agent
turns later instead of right now.
Today that primarily means cron-based schedules, triggers, workflow run
history, event publishing, and run cancelation. The workflow model is global.
A workflow target is always explicit: target.steps.
Mental model
Start by configuring at least one workflow provider under
providers.workflow, then define target.steps for the app calls and
agent turns you want Gestalt to invoke. From there you create either a
schedule, which runs on a cron expression, or a trigger, which runs when a
published event matches. The workflow provider persists that object, creates
workflow runs, and calls back into Gestalt when a run should execute. Gestalt
then executes the steps in order, and you inspect or cancel the resulting runs
through the global workflow runs surface.
There are two ownership modes. Config-managed workflows live in YAML under
top-level workflows: and execute as the system config actor. User-owned
schedules and triggers are created through the global API or CLI and
execute as the subject that created them.
Each schedule or trigger points at one ordered step plan. Use app steps for
deterministic operations, agent steps for model turns, and when for
conditional later steps. To send a prior step result through another app,
model that delivery as its own app step.
Workflow provider selection is global, not app-scoped. A config-managed
workflow object chooses a provider with provider, or inherits the sole/default
providers.workflow entry when that field is omitted. User-owned schedules and
triggers use the same provider pool through the global HTTP API and CLI.
How workflow execution uses authorization
Workflow authorization is easiest to reason about if you split it into creation time and run time. When a user creates a schedule, Gestalt checks whether the current subject is allowed to invoke the target operation right now. It then stores the owner subject plus a permission ceiling for delayed execution.
When the workflow fires later, Gestalt resolves that stored owner reference and checks authorization again before invoking the target. That means a schedule can still fail at run time if the subject lost access, if the target operation is no longer allowed, or if the original token had narrower permissions than the full subject would otherwise have.
Config-managed workflows follow the same pattern, but the owner is the system
config actor rather than a human subject. See
Authorization for the deeper
distinction between workflow execution-ref scopes, nested app invokes, and
credential resolution.
Configuring providers.workflow
You need at least one entry under providers.workflow. Workflow providers back
global runs, schedules, triggers, and events. Configure the provider once, then
reference it from config-managed workflows, CLI-created workflows, HTTP API
requests, or the app-facing Workflow.
If you configure exactly one workflow provider, or mark one as default: true,
then schedules and triggers may omit provider.
For package names, provider fields, IndexedDB bindings, and hosted runtime placement, see Config File.
Defining config-managed workflows
Config-managed workflows live under the top-level workflows: block.
Schedules
workflows:
schedules:
nightly_sync:
provider: local
cron: "0 3 * * *"
timezone: America/New_York
runAs:
subject:
id: service_account:roadmap-sync
target:
steps:
- id: sync
app:
name: roadmap
operation: sync
connection: default
instance: tenant-a
input:
mode: incremental
includeArchived: false
invokes:
- app: slack
operation: conversations.list
- app: slack
operation: conversations.historyThis creates a durable schedule named nightly_sync that invokes
roadmap.sync every day at 3:00 AM New York time. runAs
makes the runtime principal service_account:roadmap-sync; the schedule itself
remains owned by config for reconciliation. invokes grants additional app
operations the target may invoke while it runs; Gestalt always includes the
direct target operation. These grants do not create credentials.
Subject-scoped connections use credentials stored for the runAs.subject.id;
none connections require no upstream credential.
Triggers
workflows:
eventTriggers:
roadmap_item_updated:
provider: local
match:
type: roadmap.item.updated
source: roadmap
subject: item
target:
steps:
- id: notify
app:
name: slack
operation: chat.postMessage
connection: alerts
instance: engineering
input:
channel: C123456
text: "A roadmap item changed."
runAs:
subject:
id: service_account:roadmap-events
invokes:
- app: roadmap
operation: items.getThis creates a durable trigger that listens for published events with:
type = "roadmap.item.updated", plus optional exact matches for
source = "roadmap" and subject = "item". When an event matches, Gestalt
invokes slack.chat.postMessage with the static input above and records a
workflow run.
Match semantics
Trigger matching is exact string matching. match.type is required.
match.source and match.subject are optional. If source or subject is
omitted, that field is ignored during matching.
Creating user-owned schedules
User-owned schedules are global resources exposed through
POST /api/v1/workflow/schedules and gestalt workflow schedules ....
These schedules execute as the creating subject, not as a app-local or
synthetic workflow subject.
CLI
gestalt workflow schedules create \
--cron "0 */6 * * *" \
--timezone America/New_York \
--app github \
--operation issues.list \
--connection default \
--instance acme-org \
-p owner=valon-technologies \
-p repo=gestalt \
-p state=openList schedules:
gestalt workflow schedules list
gestalt workflow schedules list --app githubInspect, update, pause, resume, or delete a schedule:
gestalt workflow schedules get <schedule-id>
gestalt workflow schedules update <schedule-id> --cron "0 9 * * 1-5"
gestalt workflow schedules pause <schedule-id>
gestalt workflow schedules resume <schedule-id>
gestalt workflow schedules delete <schedule-id>The provider field is available on the HTTP API. Omit it when you have a
sole/default workflow provider; include it when you need to select a specific
workflow backend.
Creating user-owned triggers
User-owned triggers are global resources exposed through
POST /api/v1/workflow/event-triggers and
gestalt workflow triggers .... These triggers execute as the creating
subject, not as a app-local or synthetic workflow subject.
CLI
gestalt workflow triggers create \
--type roadmap.item.updated \
--source roadmap \
--subject item \
--app slack \
--operation chat.postMessage \
--connection default \
-p channel=C123 \
-p text='Roadmap item updated'Use --target-file file.json to pass the full target object directly.
List triggers:
gestalt workflow triggers list
gestalt workflow triggers list --app slack --type roadmap.item.updatedInspect, update, pause, resume, or delete a trigger:
gestalt workflow triggers get <trigger-id>
gestalt workflow triggers update <trigger-id> --type roadmap.item.synced
gestalt workflow triggers pause <trigger-id>
gestalt workflow triggers resume <trigger-id>
gestalt workflow triggers delete <trigger-id>Use --provider in the CLI or the provider field in the HTTP API when you
need to select a specific workflow backend.
Publishing workflow events
Triggers become useful once something can publish matching events into
the workflow system. Gestalt now exposes a public event-ingress route at
POST /api/v1/workflow/events and the matching CLI command
gestalt workflow events publish.
When you publish an event, Gestalt normalizes the event ID, spec version, and
timestamp if you omit them, then fans that event out across the configured
workflow providers. Triggers match on the event payload itself. In practice,
that means type is required, while source and subject remain optional
exact-match fields. This is also the main handoff mechanism for multi-step
workflows.
The public HTTP route and CLI publish to every configured workflow provider.
App code can be more specific when it already knows where triggers are
stored. The workflow SDK accepts a provider selection field on
PublishEvent (providerName in TypeScript, provider_name in Python and
Rust, ProviderName in Go); setting it publishes only to that workflow
provider. This selection names the workflow backend, not the app that owns
matching triggers. Trigger ownership and target invocation are still controlled
by the configured trigger.
CLI
gestalt workflow events publish \
--type roadmap.item.updated \
--source roadmap \
--subject item \
--data-content-type application/json \
-p id=item-1 \
-e traceId=trace-1For a provider that receives external callbacks, the usual shape is to publish an integration-neutral workflow event from the provider and let a configured trigger route it to deployer-owned code.
Using Workflow from an app
Apps and other executable provider code can use Workflow to create
reusable workflow definitions, publish workflow events, start runs, signal runs,
and create schedules or triggers from the current provider request. The SDK
attaches the request invocation token to each workflow call, so the host keeps
the same subject and permission ceiling.
Deployments can make this host-service access explicit with app workflow
capabilities. When capabilities.workflow is omitted, workflow calls
from that app are denied. Set operations to the workflow methods that
app code may call:
apps:
slack:
capabilities:
workflow:
operations:
- definitions.create
- definitions.get
- runs.signalOrStart
- schedules.create
- eventTriggers.create
- events.publishWorkflow capabilities gate the workflow method only. The workflow target still has to pass the same provider, operation, credential-mode, catalog, and runtime authorization checks as any other delayed workflow invocation.
Workflow mutating actions and event publishes emit audit records with
source=workflow_manager, including the caller app, selected workflow
backend, target object, and target authorization context. See
Audit Logging for the event names and
workflow-specific audit fields.
A workflow definition stores a reusable workflow target on a workflow provider.
Runs, schedules, and event triggers that reference a definition_id use that
provider and snapshot the target when they are created, so later edits to the
definition do not silently change existing automation.
Go
import (
"context"
gestalt "github.com/valon-technologies/gestalt/sdk/go"
)
func publishSlackEvent(
ctx context.Context,
request gestalt.Request,
slackEvent map[string]any,
) (string, error) {
workflows, err := request.Workflow()
if err != nil {
return "", err
}
defer workflows.Close()
published, err := workflows.PublishEvent(ctx, gestalt.WorkflowPublishEvent{
ProviderName: "local",
Event: &gestalt.WorkflowEvent{
Type: "slack.event.received",
Source: "slack",
Subject: "route:knowledge-ingest",
DataContentType: "application/json",
Data: map[string]any{"event": slackEvent},
},
})
if err != nil {
return "", err
}
return published.ID, nil
}The same client also exposes start-run, signal-run, signal-or-start-run, and
schedule or trigger management methods in each SDK. Use providerName /
provider_name / ProviderName only when the deployment has more than one
workflow provider or the caller needs to target a specific backend.
workflows:
eventTriggers:
slack_knowledge_ingest:
provider: local
match:
type: slack.event.received
source: slack
subject: route:knowledge-ingest
target:
steps:
- id: ingest
app:
name: knowledge
operation: ingestSlackEvent
input:
sourceId: slack-public
invokes:
- app: knowledge
operation: tokens.createThe target operation reads the published event from the workflow trigger
context, for example request.workflow.trigger.event in TypeScript. Keeping the
callback provider generic and routing through workflow events lets each deployer
own the ingestion operation in whatever SDK language their app uses.
Composing larger workflows
Use target.steps for ordered work that should run inside one workflow
execution. Use workflow events when separate schedules, triggers, or workflows
should compose across execution boundaries.
One common cross-workflow pattern is to use a schedule to start the first workflow, then have one of its steps publish a completion event. Other triggers listen for that event and start follow-up workflows.
workflows:
schedules:
nightly_sync:
provider: local
cron: "0 3 * * *"
timezone: UTC
target:
steps:
- id: sync
app:
name: roadmap
operation: sync
input:
mode: incremental
eventTriggers:
notify_sync_completed:
provider: local
match:
type: roadmap.sync.completed
source: roadmap
target:
steps:
- id: notify
app:
name: slack
operation: chat.postMessage
connection: alerts
input:
channel: C123456
text: "Nightly roadmap sync completed."
refresh_search_index:
provider: local
match:
type: roadmap.sync.completed
source: roadmap
target:
steps:
- id: reindex
app:
name: search
operation: reindex
connection: default
input:
scope: roadmapIn that example, nightly_sync only starts the first step. The
roadmap.sync operation is responsible for publishing
roadmap.sync.completed when it finishes successfully. Once that event exists,
both triggers match it and the flow fans out into Slack notification and search
reindexing.
You can publish that handoff event from outside Gestalt through
POST /api/v1/workflow/events, or from app code through the app-facing
workflow PublishEvent call. External publishing is useful when some
other system already knows the step finished. App-side publishing is useful
when the operation itself owns the next handoff.
Inspecting and canceling workflow runs
Runs are the execution records produced by schedules, triggers, or other
workflow-provider actions. The global run surface is
GET /api/v1/workflow/runs, GET /api/v1/workflow/runs/{runID},
POST /api/v1/workflow/runs/{runID}/cancel, and
gestalt workflow runs ....
CLI
gestalt workflow runs list
gestalt workflow runs list --app github --status failed
gestalt workflow runs get <run-id>
gestalt workflow runs cancel <run-id> --reason "operator requested"Run responses include id, provider, status, target, trigger,
createdBy, createdAt, startedAt, completedAt, statusMessage, and
resultBody. The trigger.kind value is schedule, event, or manual.
If you are implementing a provider or another event producer, the
Building your own workflow provider
section covers the PublishEvent interface and provider-side contract.
Execution identity depends on how the workflow was created.
Config-managed schedules and triggers are reconciled as system:config. At
runtime they execute as runAs.subject.id when runAs
is declared, otherwise as system:config. User-owned schedules and triggers
execute as the creating subject.
Building your own workflow provider
Manifest
A workflow provider manifest declares kind: workflow:
kind: workflow
source: github.com/your-org/workflow/example
version: 0.0.1
displayName: Example Workflow
description: Workflow provider with custom run and schedule storage.
spec:
configSchemaPath: ./workflow_config.jsonProvider interface
The workflow protocol is a state and execution surface. A provider stores
workflow definitions, runs, schedules, triggers, and signals, then executes
ready target.steps plans with the workflow SDK helpers and the current
invocation token. Gestalt mints the token with the creating subject and
authorization ceiling; provider code persists normal workflow objects and uses
the SDK to invoke app and agent steps when a run advances.
The full protocol includes:
| Method | Purpose |
|---|---|
StartRun / GetRun / ListRuns / CancelRun | Manage workflow runs |
SignalRun / SignalOrStartRun | Attach signals to an existing run or create a keyed run before signaling it |
CreateDefinition / GetDefinition / UpdateDefinition / DeleteDefinition | Manage reusable workflow definitions |
UpsertSchedule / GetSchedule / ListSchedules / DeleteSchedule / PauseSchedule / ResumeSchedule | Manage schedules |
UpsertEventTrigger / GetEventTrigger / ListEventTriggers / DeleteEventTrigger / PauseEventTrigger / ResumeEventTrigger | Manage triggers |
PublishEvent | Deliver an event into the provider |
Go, Python, Rust, and TypeScript expose the workflow provider service through SDK-owned request and response types. The SDK runtime owns transport conversion; provider code should focus on durable run, schedule, trigger, signal, and definition state plus step execution.
The examples below show the authored shape with representative methods. A production provider should implement every method in the protocol table.
Go
package exampleworkflow
import (
"context"
gestalt "github.com/valon-technologies/gestalt/sdk/go"
)
type Provider struct {
gestalt.UnimplementedWorkflowProvider
}
func New() *Provider { return &Provider{} }
func (p *Provider) Configure(context.Context, string, map[string]any) error {
return nil
}
func (p *Provider) StartRun(_ context.Context, req *gestalt.StartWorkflowProviderRunRequest) (*gestalt.BoundWorkflowRun, error) {
return &gestalt.BoundWorkflowRun{
ID: "workflow:run-1",
Status: gestalt.WorkflowRunStatusValuePending,
Target: req.Target,
}, nil
}
func (p *Provider) UpsertSchedule(_ context.Context, req *gestalt.UpsertWorkflowProviderScheduleRequest) (*gestalt.BoundWorkflowSchedule, error) {
return &gestalt.BoundWorkflowSchedule{
ID: req.ScheduleID,
Cron: req.Cron,
Target: req.Target,
Paused: req.Paused,
}, nil
}
func (p *Provider) PublishEvent(context.Context, *gestalt.PublishWorkflowProviderEventRequest) error {
return nil
}Production workflow providers should persist returned IDs, timestamps, statuses, created-by actors, definition IDs, and signal sequence numbers exactly as stored. When a schedule or trigger is ready to run, the provider creates or updates run state and executes the resolved target steps with the workflow SDK executor.
Signals and step execution
Signals let a workflow caller deliver additional payloads into a run after it
has started. SignalRun targets an existing run_id. SignalOrStartRun
accepts a workflow_key, target, idempotency key, and signal. It should return
the run, the stored signal, and started_run: true when it created the run.
This is useful for workflows where many external events should coalesce into
one active run.
The invocation token on provider requests carries the subject and authorization
context needed for delayed work. Providers should persist the token with the run
or provider-owned workflow object and pass it to the SDK executor when advancing
steps. The executor evaluates workflow values, applies when/skip behavior,
invokes app steps, polls agent turns, and returns the JSON step-result envelope
that providers store on completed or failed runs.
Deploying the provider
Publish a workflow provider as a provider package, then configure it under
providers.workflow. Config-managed schedules and triggers continue to live
under the top-level workflows: block. For package publishing, lock/sync, and
release behavior, see Releasing provider packages.
What to read next
If you want the surrounding config model, read Configuration and the full Config File reference. For route and command details, see HTTP API and CLI. The Built-in Providers reference lists first-party workflow provider packages.