Skip to main content

How Actyx works

Actyx implements local-first cooperation, which in short means that it allows programs on different computers to work together — directly between those computers without the cloud.

The example process

This conceptual guide explains how Actyx works with a simple example process: A workpiece is processed by a machine, and then packaged by a robot:


The traditional programming model

Traditionally, you would write two apps, one dealing with each asset. These apps would then be connected to each other with a database or broker:


The local-twin programming model

With the Actyx Platform, you program the process as autonomous local twins that publish and consume events, and develop states based on these events:


Local twins are implemented in the Typescript programming language using APIs provided by the Actyx Pond library. There are two different ways in which the business logic can be formulated:

  • If the local twin models something where your application shall control or coordinate the progression through a sequence of different life cycle states then you use fishes.
  • Otherwise you can use stateless logic, emit events as they occur in the real world, and retrieve information using the Actyx Query Language.

Stateless Local Twins

In the example above, the machine typically manages its own life cycle and the local twin statelessly follows suit:

// first declare tags, optionally with enforced event types
const machine = Tag('machine:4711')
const idle = Tag<{ idle: boolean }>('machineIdle')
const counter = Tag<{ goodPieces: number; scrapPieces: number }>('machineCounter')

machineConnector // assuming this is an EventEmitter
.on('idleStatus', (idle: boolean) => pond.publish(machine.and(idle).apply({ idle })))
.on('counterUpdate', (goodPieces: number, scrapPieces: number) =>
pond.publish(machine.and(counter).apply({ goodPieces, scrapPieces })),
.on('machined', (workPieceId: string, result: 'good' | 'scrap') =>
// WorkPieceTag introduced below
type: 'machined', machine: '4711', result,
error handling

The publish() calls return a Promise that you would typically attach an error handler to. We leave that out here to focus on the main part.

Obtaining the latest idle state of a machine is then only a matter of running the following AQL query:

const [{ idle }] = await pond.event().queryAql({
query: `
PRAGMA features := aggregate
FROM "machineIdle" & "machine:4711" AGGREGATE LAST(_)

Stateful Local Twins

We call the stateful kind of twins fishes. The logic of a fish is defined by three things:

  1. The fish state, frequently implemented as a tagged union (i.e. an enum with data).

    // Possible states of the work piece
    export type Fresh = { type: 'fresh' }
    export type Scrapped = { type: 'scrapped'; machine: string }
    export type Machined = { type: 'machined'; machine: string }
    export type Packaged = { type: 'packaged'; machine: string; packager: string }
    export type WorkPieceState = Fresh | Machined | Packaged
  2. The events that may influence this state, including those created by other fish.

    export type MachinedWorkPiece = { type: 'machined'; machine: string; result: 'good' | 'scrap' }
    export type PackagedWorkPiece = { type: 'packaged'; packager: string }
    export type WorkPieceEvent = MachinedWorkPiece | PackagedWorkPiece
    export const WorkPieceTag = Tag<WorkPieceEvent>('workPiece')
  3. An event reducer that updates the fish's state when a new event is received.

    const onEvent: Reduce<WorkPieceState, WorkPieceEvent> = (state, event) => {
    switch (state.type) {
    case 'fresh': {
    switch (event.type) {
    case 'machined':
    // in the `fresh` state the work piece can only be machined or scrap
    return event.result === 'good'
    ? { type: 'machined', machine: event.machine }
    : { type: 'scrapped', machine: event.machine }
    case 'machined': {
    switch (event.type) {
    case 'packaged':
    // in the `machined` state the work piece can only be packaged
    return { type: 'packaged', machine: state.machine, packager: event.packager }
    // ignore all unknown or inappropriate events
    return state

Now, if a packaged event were to arrive before a machined event has been seen (e.g. because the machine’s network connection is currently slow), that event would be ignored — for now: if at a later time the missing event from the machine arrives, the fish reverts to the fresh state and processes its now more complete event log again, this time reaching the packaged state.

The above pieces are tied together with an event subscription to form a proper Fish factory like so:

export const WorkPieceFish = (workPieceId: string): Fish<WorkPieceState, WorkPieceEvent> => {
fishId: FishId.of('workPiece', workPieceId, 1),
where: WorkPieceTag.withId(workPieceId),
initialState: { type: 'fresh' },

The local computing environment

After you have programmed the local twins, you create local computing environments using edge devices. The local computing environment provides the infrastructure necessary for running local twins. It is a hardware/software combination.

The hardware can be any mobile device, PLC, or PC running Linux, Android, Windows or Docker:

  • Tablets: Panasonic, Zebra, Samsung
  • PLCs: Phoenix, Beckhoff, Weidmüller
  • PCs: any

The software is Actyx. It runs on each device and acts as a decentralized infrastructure which provides data dissemination, data persistence, and runtimes.


In this example, you could deploy Actyx to a small industrial PC that is connected to the machine (or directly to the machine's PLC) and deploy Actyx to a small industrial PC that you connect to the robot.

Deployment of twins as apps

Twins are packaged into apps that are deployed to the edge devices. Apps are the unit of deployment and contain twins as well as code that interacts with them:

  • User interfaces for human interaction
  • Machine integrations (e.g. OPC UA, I/Os)
  • Software integrations (e.g. ERP, Cloud)


Local interaction

After you have deployed the apps to the edge devices running Actyx, Twins interact and cooperate locally:


Due to the local interaction of the twins, there is no dependency between environments.


Synchronization of local twins

When edge devices are connected, Actyx automatically synchronizes the twins in real-time:


The twins' history is consistent and forever accessible:


Add new twins to the process

To extend or scale the process, you simply add new local twins:


Where next?