automation
v0.1.27
Kickside automations — the generic install/control/uninstall engine for the unified automation model (component-backed instances, id-based rollback, reflection-dispatched controls, cron-backed scheduled actions). Domain types are contributed by their own modules.
Run
wippy run kickside/automationKickside Automation
The generic engine for the unified automation model. An automation is anything installable — a recurring agent run, a knowledge base, or a business process log. This module owns the lifecycle machinery; the domain types live in their own modules.
Model
- Type = a
contract.binding(meta.type: kickside.automation) implementingkickside.automation:installable(install(input) -> {state, metadata}) andkickside.contract:deletable(delete, defaulting tonoop_delete). - Instance = a row in the shared
componentstable (impl_id= the binding,private_context= opaque install state incl. the_rollbackchain,component_meta= display fields,class = automation). Visible/listable like any component; per-instance status is read through the universalkickside.contract:component:get_status, not an automation-specific hook. - Controls (pause/resume/trigger/update/…) are plain methods implemented by
a type's contracts and explicitly published through
meta.actions. Contract methods that are not listed inmeta.actionsremain internal. - Scheduling is delegated to
kickside.cron: cron owns timing, retries, and execution history; automation owns component identity, private state, action dispatch, and rollback. Cron's row id istask_id; automation stores that row reference asschedule_idin private state and rollback payloads. - Ports are the automation I/O surface. Domain modules publish first-class
registry.entryports withmeta.type: kickside.automation.port. A store port declaresoperationsand flows items in throughkickside.data:writable; an events port declares aneventreference and a collection port is backed bykickside.data:pullable. Poll sources keep their cursor in the automation instance'sprivate_context; events sources are thread events consumed through a projection owned by the trigger service. - Trigger subscriptions are binding-owned automation machinery. A binding
subscribes to a published events port by registering the source thread and its
binding id through the trigger service. The shared worker filters source events
and delivers canonical envelopes directly to that binding; dispatch authority is
revalidated at the binding-to-Flow boundary. Automation install scripts compose
state via
kickside.automation:lib, schedule viakickside.automation:schedule, pull viakickside.data:pullable, write viakickside.data:writable, and transform viakickside.transform:transform. Each created external resource records rollback before install can complete. - Pullable conformance: provider test suites that implement
kickside.data:pullableshould importkickside.automation:pullable_conformanceand pass their pull function plus fixture config. The kit reads the canonicalkickside.data:pullableschemas from the registry and checks item envelopes, cursor round-trip, empty-page retry hints, DataError taxonomy, and explicitbackfill_sincebehavior. - Events vs ports are two layers. A binding's
events.input/outputis the engine-layer thread-event ABI: what the component log accepts or emits for projections, retention, trace, and audit. A port is a first-classregistry.entry(meta.type: kickside.automation.port) — the curated, connectable catalog on top of a binding. A store port (operations, backed bykickside.data:writable) flows items in; a collection port (backed bykickside.data:pullable) or an events port (aneventreference to a thread-event entry) flows items out. Publication is existence: an internal or audit event simply has no port. A port's data faces are itsinput_schema/output_schema— an events port derives its output face from the referenced event entry's schema — and its config face isconfig_schema. - Flows are a product surface over ports, not a separate engine. A data-flow/workload diagram should render source -> transform -> sink nodes from published port entries, with thread events, projections, trace context, cron, and component identity doing the execution underneath.
Lifecycle
External-write declaration example
External delivery stays in the existing event-to-Port grammar. An Automation
subscribes to a published brief.approved Trigger, maps that event into a
provider-object payload, and targets the writable HubSpot CRM Port. The output
is passed to the sink as { config, sink_op, input, idempotency_key }; the event
id is the idempotency key.
title: Write approved brief to HubSpot
trigger:
v: 1
source: spiralscout.brief:port.approved
config: { thread_id: brief-thread-id }
backfill: none
step:
backend_ref: spiralscout.brief:approved_to_hubspot_payload
config: {}
sink:
port: kickside.hubspot.sink:crm
config: { connection_id: conn-hubspot, object_type: contacts }
The writable sink owns provider I/O. On success, a CRM-aware sink appends
spiralscout.crm.events:external_write.succeeded to the CRM thread with the
same idempotency key as write_ref, per-field canonical-value hashes, and its
workflow/approval origin. CRM then materializes the event into its own
echo-suppression projection; there is no outbox or sidecar contract in between.
- install: validate input against
meta.inputswhen declared (includingtype: password/format: password/sensitive: truerender hints for masked setup fields) → run the binding's install body (provisions via the relevant domain contracts, recording reversible{target, args}steps) → persist state+metadata+rollback as a component. If the install result declaresschedules, the engine creates those cron rows after component registration, targetskickside.automation:scheduled_action, stores returned ids in private state (schedule_idsplus any explicitstate_key), and appends schedule deletion to rollback. Mid-install failure replays the chain. - list / read: ACL-scoped components of
class=automation; status via the universal componentget_status. - state: private durable state lives in component
private_context; the frontend-safe projection lives in component public meta.GET /{id}/statereads only declared public fields through component access checks. Type metadata declaresmeta.public_state_schema; install bootstrap and typed action/projection workers are the only writers for those public fields. New schemas should use specific state keys (run_state,connection_state,account_state, etc.) instead of overloadingstatus; existingstatusfields remain stable until an explicit persisted-key migration. - custom create UI: a type may declare
meta.component.create.view; the automation manager mounts the resolved web component and refreshes when it emitsautomation-installed. - actions:
POST /{id}/actions/{method}opens the automation component (private_context → ctx) and dispatches only methods declared in the binding'smeta.actionsallowlist. - delete: replay the rollback chain in reverse via
funcs(undo targets must be idempotent / not_found=success), then remove the component. - owned resources: a type may declare
meta.owned_resourcesentries ({kind, state_key}) so a child resource module can ask the automation layer to tear down its owning umbrella by{resource_kind, resource_id}without knowing private state field names. - scheduled execution: cron opens
kickside.automation:scheduled_actionunder the schedule's captured actor/scope and callsexecute; the adapter dispatches the configured action on the automation component. - source events: event-driven automations subscribe to a source component's thread by registering a projection. The projection runner preserves trace context from input events, and the trigger worker reconstructs the automation installer's frozen execution identity before writing to the selected sink.
Requirements
api_router(requiredns.requirement) — firewalled router the endpoints attach to.- Uses
kickside.componentthrough its declared component dependency. - Depends on
kickside/cronfor the canonical schedulable adapter and schedule helper. Domain provisioning contracts (threads/kb/connections/uploads/etc.) are still called by the types, so the engine stays resource-agnostic and boots with zero registered types. Core owns threads, projections, trace, and deferred execution identity; automation owns source selection and per-source progress.