Skip to Content
ProvidersWorkflow

Workflow

Workflows are under active development and are not yet stable. Breaking changes may happen between releases without warning. Feedback and bug reports are welcome via GitHub Issues .

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.history

This 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.get

This 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.

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=open

List schedules:

gestalt workflow schedules list gestalt workflow schedules list --app github

Inspect, 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.

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.updated

Inspect, 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.

gestalt workflow events publish \ --type roadmap.item.updated \ --source roadmap \ --subject item \ --data-content-type application/json \ -p id=item-1 \ -e traceId=trace-1

For 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.publish

Workflow 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.

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.create

The 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: roadmap

In 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 ....

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.json

Provider 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:

MethodPurpose
StartRun / GetRun / ListRuns / CancelRunManage workflow runs
SignalRun / SignalOrStartRunAttach signals to an existing run or create a keyed run before signaling it
CreateDefinition / GetDefinition / UpdateDefinition / DeleteDefinitionManage reusable workflow definitions
UpsertSchedule / GetSchedule / ListSchedules / DeleteSchedule / PauseSchedule / ResumeScheduleManage schedules
UpsertEventTrigger / GetEventTrigger / ListEventTriggers / DeleteEventTrigger / PauseEventTrigger / ResumeEventTriggerManage triggers
PublishEventDeliver 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.

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.

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.