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.

BUSL-1.1 2.2k downloads
Updated 3 days ago Repository
kicksideautomationlifecyclecomponentsscheduler

Run

wippy run kickside/automation

Kickside 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) implementing kickside.automation:installable (install(input) -> {state, metadata}) and kickside.contract:deletable (delete, defaulting to noop_delete).
  • Instance = a row in the shared components table (impl_id = the binding, private_context = opaque install state incl. the _rollback chain, component_meta = display fields, class = automation). Visible/listable like any component; per-instance status is read through the universal kickside.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 in meta.actions remain 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 is task_id; automation stores that row reference as schedule_id in private state and rollback payloads.
  • Ports are the automation I/O surface. Domain modules publish first-class registry.entry ports with meta.type: kickside.automation.port. A store port declares operations and flows items in through kickside.data:writable; an events port declares an event reference and a collection port is backed by kickside.data:pullable. Poll sources keep their cursor in the automation instance's private_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 via kickside.automation:schedule, pull via kickside.data:pullable, write via kickside.data:writable, and transform via kickside.transform:transform. Each created external resource records rollback before install can complete.
  • Pullable conformance: provider test suites that implement kickside.data:pullable should import kickside.automation:pullable_conformance and pass their pull function plus fixture config. The kit reads the canonical kickside.data:pullable schemas from the registry and checks item envelopes, cursor round-trip, empty-page retry hints, DataError taxonomy, and explicit backfill_since behavior.
  • Events vs ports are two layers. A binding's events.input/output is the engine-layer thread-event ABI: what the component log accepts or emits for projections, retention, trace, and audit. A port is a first-class registry.entry (meta.type: kickside.automation.port) — the curated, connectable catalog on top of a binding. A store port (operations, backed by kickside.data:writable) flows items in; a collection port (backed by kickside.data:pullable) or an events port (an event reference 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 its input_schema/output_schema — an events port derives its output face from the referenced event entry's schema — and its config face is config_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.inputs when declared (including type: password / format: password / sensitive: true render 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 declares schedules, the engine creates those cron rows after component registration, targets kickside.automation:scheduled_action, stores returned ids in private state (schedule_ids plus any explicit state_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 component get_status.
  • state: private durable state lives in component private_context; the frontend-safe projection lives in component public meta. GET /{id}/state reads only declared public fields through component access checks. Type metadata declares meta.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 overloading status; existing status fields 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 emits automation-installed.
  • actions: POST /{id}/actions/{method} opens the automation component (private_context → ctx) and dispatches only methods declared in the binding's meta.actions allowlist.
  • 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_resources entries ({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_action under the schedule's captured actor/scope and calls execute; 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 (required ns.requirement) — firewalled router the endpoints attach to.
  • Uses kickside.component through its declared component dependency.
  • Depends on kickside/cron for 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.