This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Service reference

Look up injectable AngularTS services and their runtime responsibilities.

Service names beginning with $ are framework-provided injection tokens. Read the service guides first if services or dependency injection are new to you.

1 - $anchorScroll

Scroll to document anchors and coordinate hash-driven scrolling.

$anchorScroll scrolls to the element identified by a hash or, when omitted, to the current value of $location.getHash(), according to the rules specified in the HTML spec.

It also watches the URL hash and automatically scrolls to any matched anchor whenever it is changed by $location.

Exact signatures live in TypeDoc:

Configure

Use module.config({ $anchorScroll: ... }) for application-wide scroll policy.

angular.createModule("app", []).config({
  $anchorScroll: {
    autoScrolling: false,
  },
});

Executable sample: anchor-scroll.html

Manual Scrolling

$location.setHash("details");
$anchorScroll("details");

When automatic scrolling is disabled, call $anchorScroll(...) directly after coordinating routing, animation, or focus.

Vertical Offset

Use the yOffset property to specify a fixed or dynamic vertical scroll offset. This is useful when the application has sticky headers.

Example

<style>
  .demo2 {
    height: 100px;
    overflow: auto;
  }

  .anchor {
    border: 2px dashed DarkOrchid;
    padding: 10px 10px 400px 10px;
  }

  .fixed-header {
    background-color: rgba(0, 0, 0, 0.2);
    height: 50px;
  }

  .fixed-header > a {
    display: inline-block;
    margin: 5px 15px;
  }
</style>

<section id="demo2" class="demo2">
  <div ng-controller="headerCtrl" class="fixed-header">
    <button class="btn" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]">
      Go to anchor {{x}}
    </button>
  </div>
  <div id="anchor{{x}}" class="anchor" ng-repeat="x in [1,2,3,4,5]">
    Anchor {{x}} of 5
  </div>
</section>
window.angular
  .createModule('demo2', [])
  .run([
    '$anchorScroll',
    function ($anchorScroll) {
      $anchorScroll.yOffset = 100; // always scroll by 50 extra pixels
    },
  ])
  .controller('headerCtrl', [
    '$anchorScroll',
    '$location',
    '$scope',
    function ($anchorScroll, $location, $scope) {
      $scope.gotoAnchor = function (x) {
        window.$locationTest = $location;
        const newHash = 'anchor' + x;
        if ($location.getHash() !== newHash) {
          // Pass `newHash` to setHash() and
          // $anchorScroll will automatically scroll to it
          $location.setHash('anchor' + x);
        } else {
          // call $anchorScroll() explicitly,
          // since getHash() still returns the same value
          $anchorScroll();
        }
      };
    },
  ]);

window.angular.bootstrap(document.getElementById('demo2'), ['demo2']);
Anchor {{x}} of 5

For detailed method description, see $anchorScroll.

2 - $compile

Template compilation service

3 - $controller

Controller service

4 - $cookie

Provides read/write access to browser’s cookies

Description

The $cookie service offers a simple API for interacting with browser cookies in AngularTS applications. It allows you to:

  • Read existing cookies
  • Write new cookies
  • Delete cookies
  • Store and retrieve JavaScript objects (via JSON serialization)
  • Automatically URI-encode and decode values

Example

angular.getModule('app').controller(
  'UserCtrl',
  /** @param {ng.CookieService} $cookie */
  ["$cookie", function ($cookie) {
    // Write cookie
    $cookie.put('session_id', 'abc123');

    // Read cookie
    const session = $cookie.get('session_id');

    // Store object
    $cookie.putObject('profile', { name: 'Alice', admin: true });

    // Read object
    const profile = $cookie.getObject('profile');

    // Remove cookie
    $cookie.remove('session_id');
  }],
);

Cookie behavior can be customized globally with module.config({ $cookie: ... }).

angular.createModule("app", []).config({
  $cookie: {
    defaults: {
      path: "/",
      secure: true,
      samesite: "Lax",
    },
  },
});

Defaults are merged into $cookie.put(), $cookie.putObject(), and $cookie.remove() calls. Per-call options still override configured defaults.

Cookies are limited to roughly 4 KB, including key and value, so avoid storing large objects; prefer identifiers or short tokens.

Executable sample: log-cookie.html

For detailed method description, see CookieService

5 - $eventBus

Application-wide event bus service

Description

A messaging service, backed by an instance of EventBus. This implementation is based on the original Google Closure EventBus but uses queueMicrotask instead of Window.setTimeout() for its async implementation.

$eventBus allows communication between an Angular application instance and the outside context, which can be a different module, a non-Angular application, a third-party party library, or even a WASM application. Additionally, $eventBus can be used to communicate directly with a template, using ng-channel directive.

$eventBus should not be used for communicating between Angular’s own primitives.

  • For sharing application state: use custom Services and Factories that encapsulate your business logic and manage your model.
  • For intercomponent communication: use $scope.on(), $scope.emit(), and $scope.broadcast() methods.

The example below demonstrates communication between the global window context and a controller. Note: Ensure topic clean-up after the $scope is destroyed

Example

<section ng-app="demo">
  <div ng-controller="DemoCtrl as $ctrl">
    Milliseconds elapsed since the epoch <b>{{ $ctrl.ms }}</b>
  </div>
</section>

<!--We are using a regular onclick and `angular` global -->
<button
  class="btn btn-dark"
  onclick="angular.eventBus.publish('demo', Date.now())"
>
  Publish time
</button>
window.angular.createModule('demo', []).controller(
  'DemoCtrl',
  class {
    static $inject = ['$eventBus', '$scope'];
    constructor($eventBus, $scope) {
      const unsubscribe = $eventBus.subscribe('demo', (val) => {
        // `this.ms = val` will not work because `this` is not a proxy
        //  to trigger change detection, we access controller as a scope property
        $scope.$ctrl.ms = val;
      });

      $scope.on('$destroy', unsubscribe);
    }
  },
);

Demo

Milliseconds elapsed since the epoch {{ $ctrl.ms }}

For detailed method description, see EventBus


6 - $exceptionHandler

Error handling service

Description

$exceptionHandler is the central hook for exceptions that escape detached AngularTS-managed work. This includes DOM and transport events, scheduled tasks, subscriptions, expression evaluation, and lifecycle callbacks whose original caller can no longer receive a thrown value or rejected promise.

The decision is: can the initiating caller still receive the failure? If yes, throw synchronously or reject the returned promise. Use $exceptionHandler only after that caller boundary no longer exists.

Synchronous API validation throws directly. Promise-returning operations reject their returned promise. Expected transport, workflow, routing, and domain failures remain in their typed operation channels.

By default, it rethrows exceptions that occur during AngularTS-managed execution. This fail-fast behavior ensures errors are visible immediately in development and in unit tests.

For type description, see ng.ExceptionHandler.

Configure

Use module.config({ $exceptionHandler: ... }) for application-wide error reporting policy.

angular.createModule('app', []).config({
  $exceptionHandler: {
    handler(error) {
      myReporter.capture(error);
      throw error;
    },
  },
});

Custom handlers should rethrow after reporting. AngularTS treats $exceptionHandler as fail-fast, and swallowing errors can hide broken application state.

Executable sample: exception-handler.html

7 - $http

HTTP client service

The $http service is AngularTS’s built-in HTTP client. Use this page for the behavioral model and common examples. For exact method signatures, parameters, return types, and interfaces, use the generated TypeDoc reference:

Usage

Call $http with a full request configuration when you need explicit control:

$http<User>({
  method: 'GET',
  url: '/api/users/42',
}).then((response) => {
  user = response.data;
});

For common HTTP verbs, use shorthand methods:

$http.get<User>('/api/users/42').then(({ data }) => {
  user = data;
});

$http.post<User>('/api/users', {
  name: 'Ada',
  email: 'ada@example.com',
});

Request Behavior

Plain objects are serialized as JSON by the default request transform. File, Blob, and FormData values are sent as-is so the browser can set the correct transport headers.

Query parameters are serialized by $httpParamSerializer. Arrays produce repeated keys, objects are JSON encoded, and null, undefined, and functions are omitted.

$http.get<Article[]>('/api/articles', {
  params: {
    category: 'news',
    tags: ['featured', 'breaking'],
  },
});

Response Behavior

Successful 2xx responses resolve with an HttpResponse<T>. Non-2xx responses, timeouts, aborts, and network errors reject with the same response shape, so error handlers can inspect status, statusText, headers, config, and xhrStatus.

$http.get<User>('/api/users/99').catch((error) => {
  if (error.xhrStatus === 'timeout') {
    message = 'Request timed out.';
  } else if (error.status === 404) {
    message = 'User not found.';
  }
});

Defaults

Configure defaults before bootstrap with module.config({ $http: ... }), or mutate runtime defaults through $http.defaults.

angular.createModule("app", []).config({
  $http: {
    defaults: {
      headers: {
        common: {
          Authorization: "Bearer token",
        },
      },
      withCredentials: true,
    },
  },
});

Default response transforms strip AngularJS JSON protection prefixes and parse JSON responses automatically.

Executable sample: http.html

Interceptors

Interceptors are registered with module.config({ $http: ... }). Request hooks run in registration order; response hooks run in reverse order.

angular.createModule("app", []).config({
  $http: {
    interceptors: [
      () => ({
        request(config) {
          config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`;
          return config;
        },

        responseError(rejection) {
          if (rejection.status === 401) {
            window.location.href = "/login";
          }

          return Promise.reject(rejection);
        },
      }),
    ],
  },
});

XSRF

$http reads the configured XSRF cookie and sends it in the configured XSRF header for trusted origins. Add cross-origin APIs explicitly:

angular.createModule("app", []).config({
  $http: {
    xsrfTrustedOrigins: ["https://api.example.com"],
  },
});

8 - $interpolate

Compile interpolation text into reusable expression functions.

9 - $location

Normalize and update browser URLs across HTML5 and hashbang modes.

$location parses the browser URL into path, search, hash, and state pieces. Changes made through $location update the browser address bar, and browser navigation updates the service.

Exact signatures live in TypeDoc:

Read The Current URL

$location.getPath();
$location.getSearch();
$location.getHash();
$location.getUrl();
$location.absUrl();

Update The URL

$location
  .setPath("/settings")
  .setSearch({ tab: "profile" })
  .setHash("details");

Setter methods return $location, so updates can be chained.

Use setSearchParam() when only one query parameter should change. Passing null or undefined removes that parameter.

$location.setSearchParam("page", 2);
$location.setSearchParam("filter", null);

$locationChangeStart is broadcast on $rootScope before the URL changes. Call event.preventDefault() from a listener to cancel the navigation.

$locationChangeSuccess is broadcast after the URL changes. In HTML5 mode, listeners may also receive the new and old history state values when the browser supports the History API.

$rootScope.on("$locationChangeStart", (event, newUrl, oldUrl) => {
  if (shouldBlock(newUrl, oldUrl)) {
    event.preventDefault();
  }
});

$rootScope.on("$locationChangeSuccess", (_event, newUrl) => {
  analytics.track("page_view", { url: newUrl });
});

Configure

Use module.config({ $location: ... }) for application-wide URL policy.

angular.createModule("app", []).config({
  $location: {
    html5Mode: {
      enabled: true,
      requireBase: false,
      rewriteLinks: true,
    },
    hashPrefix: "!",
  },
});

html5Mode may also be a boolean when only the enabled flag should change:

angular.createModule("app", []).config({
  $location: {
    html5Mode: false,
  },
});

Executable sample: location.html

10 - $log

Logging service

Using $log

Default implementation of LogService safely writes the message into the browser’s console (if present). The main purpose of this service is to simplify debugging and troubleshooting, while allowing the developer to modify the defaults for live environments.

Example*
angular.getModule('demo').controller('MyController', ["$log", ($log) => {
  $log.log('log');
  $log.info('info');
  $log.warn('warn!');
  $log.error('error');
  $log.debug('debug');
}]);

To reveal the location of the calls to $log in the JavaScript console, you can “blackbox” the AngularTS source in your browser:

Note: Not all browsers support blackboxing.
The default is to suppress debug messages unless logging config enables them.

Configure

Use module.config({ $log: ... }) for application-wide logging policy.

angular.createModule('demo', []).config({
  $log: {
    debug: true,
  },
});

debug controls whether $log.debug(...) writes to the console. The default is false.

Beacon delivery

Selected log levels can also be delivered through the browser’s Beacon API. Beacon delivery is disabled unless an endpoint is configured and defaults to the error level when levels is omitted.

angular.createModule('demo', []).config({
  $log: {
    beacon: {
      url: '/api/client-logs',
      levels: ['warn', 'error'],
    },
  },
});

The default serializer sends an application/json Blob containing timestamp, level, and args. Applications that need redaction or additional metadata can register and select a serializer:

angular
  .getModule('demo')
  .factory(
    'clientLogSerializer',
    () => (entry) =>
      JSON.stringify({
        timestamp: entry.timestamp,
        level: entry.level,
        message: String(entry.args[0]),
      }),
  )
  .config({
    $log: {
      beacon: {
        url: '/api/client-logs',
        serializer: 'clientLogSerializer',
      },
    },
  });

Beacon queueing has no response callback or delivery acknowledgement. AngularTS does not retry or persist rejected entries. Failures produce a local warning by default; set failure: "ignore" to suppress it.

Beacon delivery automatically participates in $security; there is no separate logging security switch. AngularTS checks the endpoint and configured request credentials before serializing an entry. A denied remote delivery never suppresses the local log call, and repeated policy denials produce at most one local warning.

Beacon requests cannot carry application-defined Authorization headers, so Bearer and Basic credential policies deny Beacon delivery. Use a cookie endpoint for authenticated Beacon logging. If the endpoint requires custom headers, use $http or fetch() with keepalive: true instead. Redaction and payload shaping remain the responsibility of the configured serializer.

You can also replace the logger implementation:

angular.createModule('demo', []).config({
  $log: {
    logger: () => ({
      log: console.log,
      info: console.info,
      warn: console.warn,
      error: console.error,
      debug: console.debug,
    }),
  },
});

Executable sample: log-cookie.html

Decorating $log

You can also override any $log service method with module.decorator(). Below is a simple example that overrides default $log.error to log errors to both console and a backend endpoint.

Example*
angular
  .getModule('demo')
  .decorator('$log', ["$delegate", "$http", "$exceptionHandler", ($delegate, $http, $exceptionHandler) => {
    const originalError = $delegate.error;
    $delegate.error = () => {
      originalError.apply($delegate, arguments);
      const errorMessage = Array.prototype.slice.call(arguments).join(' ');
      $http.post('/api/log/error', { message: errorMessage });
    };
    return $delegate;
  }]);

Overriding console

If your application is already heavily reliant on default console logging or you are using a third-party library where logging cannot be overriden, you can still take advantage of Angular’s services by modifying the globals at runtime. Below is an example that overrides default console.error to logs errors to both console and a backend endpoint.

Example*
angular.getModule('demo').run(["$http", ($http) => {
  /**
   * Decorate console.error to log error messages to the server.
   */
  const $delegate = window.console.error;
  window.console.error = (...args) => {
    try {
      const errorMessage = args
        .map((arg) => (arg instanceof Error ? arg.stack : JSON.stringify(arg)))
        .join(' ');
      $delegate.apply(console, args);
      $http.post('/api/log/error', { message: errorMessage });
    } catch (e) {
      console.warn(e);
    }
  };

  /**
   * Detect errors thrown outside Angular and report them to the server.
   */
  window.addEventListener('error', (e) => {
    window.console.error(e.error || e.message, e);
  });

  /**
   * Optionally: Capture unhandled promise rejections
   */
  window.addEventListener('unhandledrejection', (e) => {
    window.console.error(e.reason || e);
  });
}]);

Combining both $log decoration and console.log overriding provides a robust and flexible error reporting strategy that can adapt to your use-case.

* array notation and HTTP error handling omitted for brevity

11 - $machine

Reactive state machines with declarative transitions

$machine

$machine creates a small reactive state machine for UI and application flows. It is more than a regular service or factory because the returned machine is designed to be wrapped by AngularTS scope proxies: state and data update templates naturally, and send() batches transition work when a scope proxy is observing the machine.

Use ordinary services for IO, persistence, and shared domain logic. Use $machine when the important part is the allowed flow between states.

Use $workflow when the flow also needs command boundaries, diagnostics, snapshots, restore, retry, or repeat.

Machine naming, inference, and public-type placement follow the shared API ergonomics contract.

Create a Machine

The smallest complete game needs only a state tree and a score. Assign the machine to a controller property and let the template call send() directly:

const game = $machine({
  initial: 'ready',
  data: { score: 0 },
  states: {
    ready: { on: { start: { to: 'playing' } } },
    playing: { on: { tap: { to: 'won' } } },
    won: { on: { restart: { to: 'playing' } } },
  },
});
<section ng-controller="TapGameCtrl as $ctrl">
  <p>{{$ctrl.game.state}}: {{$ctrl.game.data.score}} / 3</p>
  <button
    ng-if="$ctrl.game.matches('ready')"
    ng-click="$ctrl.game.send('start')"
  >
    Start
  </button>
  <button
    ng-if="$ctrl.game.matches('playing')"
    ng-click="$ctrl.game.send('tap')"
  >
    Tap
  </button>
  <button
    ng-if="$ctrl.game.matches('won')"
    ng-click="$ctrl.game.send('restart')"
  >
    Play again
  </button>
</section>

state identifies the machine’s active state. State-tree transitions move to the explicit to state, while update mutates reactive game data. A to(context) resolver handles transitions whose target depends on runtime data.

Ownership Patterns

Create machines with $machine(config) and assign the returned object to the owner that should keep the flow alive.

Controller-owned machines are enough for local UI flow data:

app.controller('SessionCtrl', [
  '$machine',
  function ($machine) {
    this.session = $machine({
      initial: 'setup',
      data: { roomId: '' },
      states: {
        setup: {
          on: {
            join: {
              to: 'waiting',
              update({ data, payload }) {
                data.roomId = payload.roomId;
              },
            },
          },
        },
        waiting: {},
      },
    });
  },
]);

AppContext models can own machines that must survive DOM root destruction:

app.model('sessionRuntime', [
  '$machine',
  function ($machine) {
    return {
      session: $machine({
        initial: 'setup',
        data: { roomId: '' },
        states: {
          setup: {
            on: {
              join: {
                to: 'waiting',
                update({ data, payload }) {
                  data.roomId = payload.roomId;
                },
              },
            },
          },
          waiting: {},
        },
      }),
    };
  },
]);

Runtime adapters and game loops can keep the same machine outside the DOM:

app.controller('GameCtrl', [
  '$machine',
  function ($machine) {
    const game = $machine({
      initial: 'playing',
      data: { frame: 0 },
      states: {
        playing: {
          on: {
            tick: {
              update({ data }) {
                data.frame += 1;
              },
            },
          },
        },
      },
    });

    this.game = game;

    requestAnimationFrame(function tick() {
      game.send('tick');
      requestAnimationFrame(tick);
    });
  },
]);

Workflows can own a machine when command execution needs a local flow gate:

app.factory('sessionWorkflowMachine', [
  '$machine',
  function ($machine) {
    return $machine({
      initial: 'idle',
      data: { token: '' },
      states: {
        idle: {
          on: {
            start: {
              to: 'authenticating',
            },
          },
        },
        authenticating: {
          on: {
            authenticated: {
              to: 'ready',
              update({ data, payload }) {
                data.token = payload;
              },
            },
          },
        },
        ready: {},
      },
    });
  },
]);

Register a Named Machine

Use module.machine(name, config) when a machine should be injectable by name:

app.machine('sessionMachine', {
  initial: 'setup',
  data: {
    roomId: '',
  },
  states: {
    setup: {
      on: {
        join: {
          to: 'waiting',
          update({ data, payload }) {
            data.roomId = payload.roomId;
          },
        },
      },
    },
    waiting: {},
  },
});

app.controller('SessionCtrl', [
  'sessionMachine',
  function (sessionMachine) {
    this.session = sessionMachine;
  },
]);

You can also provide a resolvable config factory so it can read injectables at registration time:

function buildSessionMachine(appSettings) {
  return {
    initial: appSettings.initialMode,
    data: {
      roomId: '',
      error: '',
    },
    states: {
      setup: {
        on: {
          join: {
            to: 'waiting',
            update({ data, payload }) {
              data.roomId = payload.roomId;
            },
          },
        },
      },
      waiting: {},
    },
  };
}

buildSessionMachine.$inject = ['appSettings'];

app.machine('sessionMachine', buildSessionMachine);

This is useful when defaults depend on environment, user session data, or persisted configuration.

Named machines are regular DI services. The injector creates one machine instance and reuses it for that injectable name.

Template API

The machine exposes a small template-friendly API:

<button ng-disabled="!$ctrl.session.matches('setup')">Random</button>

<div ng-show="$ctrl.session.matches('waiting')">Waiting for opponent...</div>

<span>{{ $ctrl.session.data.roomId }}</span>

Runtime API

session.state; // current state string
session.data; // reactive data object
session.matches('setup'); // true when current state is setup
session.can('join', payload); // true when current state can run join
session.send('join', payload); // structured transition result
session.snapshot(); // structured clone of { state, data }
session.restore(snapshot);

send(type, payload) returns a structured result with ok, status, type, from, to, and an optional denial reason. ok is true when a transition entry exists for the current state, its optional guard passes, and its update hook runs when provided. Missing transitions and blocked guarded transitions return ok: false with a specific status and do not throw. Use $workflow when failures need diagnostics, retry, or recovery.

ok is the broad branch: successful results have status transitioned or updated; failed results have a denial, invalid-event, or missing-transition status. Machine results intentionally do not carry diagnostics. The status is the decision, while workflows own durable failure evidence.

State-tree transition shape:

  • to names the next state or resolves it from readonly transition input
  • omit to for a same-state data update
  • update({ data, payload }) mutates reactive data
  • guard({ data, payload }) blocks the transition when it returns false
  • denied({ data, payload }) may record local denial state

Routing is explicit. Static to is the normal path. Use a synchronous, side-effect-free to(context) resolver when the next state depends on runtime data, such as a game move ending in win, draw, or continued play. AngularTS resolves one target before guard and policy evaluation; hooks and update() receive that same readonly target. Return values from update() are ignored.

can(type, payload) checks the transition table, target resolver, policy, and any optional guard for the current state. It does not run update or denied. Target resolvers and guards should be side-effect-free because templates may call can() repeatedly.

Status values are transitioned, updated, missing-transition, guard-denied, policy-denied, and invalid-event. When policy denies a transition, the result includes the policy decision’s optional reason.

Denial paths are narrow. guard-denied only means the transition guard returned false; transition-local denied() runs only for that guard-denial case. Missing transitions and invalid event names do not run before, update, after, denied, enter hooks, exit hooks, or global transition hooks. policy-denied is reserved for framework policy gates. It does not run denied, mutation hooks, state hooks, global transition hooks, or legacy transition targets.

Transition Policy

Use policy when the application has a cross-cutting synchronous gate that should apply to every transition in a machine:

const session = $machine({
  id: 'session',
  initial: 'setup',
  data: { roomId: '' },
  states: {
    setup: {
      on: {
        join: {
          to: 'waiting',
          update({ data, payload }) {
            data.roomId = payload.roomId;
          },
        },
      },
    },
    waiting: {},
  },
  policy(context) {
    if (context.type === 'join' && !context.payload.roomId) {
      return { type: 'deny', reason: 'room_required' };
    }

    return 'allow';
  },
});

session.send('join', { roomId: '' }).status; // "policy-denied"

Policy contexts include operation: "machine.transition", machineId, type, from, to, event-narrowed payload, readonly data, an observational machine view, and optional meta. The policy and guard machine view exposes state, data, can(), matches(), and snapshot() but cannot dispatch or restore live state. Policies must be synchronous so can() and send() stay synchronous. Put async authorization, retries, persistence, and recovery in $workflow or another service and then project the result back through a machine event.

State-tree transitions with static to give policy the strongest pre-execution context. Legacy callback transitions do not run their target just to discover a future state, so policy sees to as the current state there.

Guarded Transitions

Use guard when a transition has a cheap synchronous condition that templates or controllers should be able to ask about through can():

const session = $machine({
  initial: 'setup',
  data: {
    roomId: '',
    inviteCode: '',
  },
  states: {
    setup: {
      on: {
        join: {
          to: 'waiting',
          guard({ data, payload }) {
            return !!data.inviteCode && payload.roomId !== '';
          },
          update({ data, payload }) {
            data.roomId = payload.roomId;
          },
          denied({ data }) {
            data.roomId = '';
          },
        },
      },
    },
    waiting: {},
  },
});

session.can('join', { roomId: 'abc' }); // evaluates guard only
session.send('join', { roomId: 'abc' }); // runs update when guard passes

Guards and updates are synchronous. Put async work, retries, diagnostics, and command recovery in $workflow, then use a machine transition to project the resulting state/data into the UI.

Example: Session Wizard

A machine is also useful for local wizard flow where the UI should only expose legal next actions:

const wizard = $machine({
  initial: 'profile',
  data: { name: '' },
  states: {
    profile: {
      on: {
        next: {
          to: 'complete',
          guard: ({ data }) => data.name.trim() !== '',
        },
      },
    },
    complete: {},
  },
});

Static to keeps the flow inspectable: next, back, and submit declare their route before mutation code runs. The same-state accept event omits to because it only updates wizard data.

Example: Tic Tac Toe

A machine is useful when user actions must follow a small set of legal states. This example keeps a tic tac toe game in playing until a move produces a win or draw. Once the machine reaches xWon, oWon, or draw, there is no move transition for that state, so further moves return false.

const game = $machine({
  initial: 'playing',
  data: {board: Array(9).fill('-'), nextPlayer: 'X'},
  states: {
    playing: {on: {move: {
      guard: ({data, payload}) => data.board[payload.index] === '-',
      update({data, payload}) {
        data.board[payload.index] = data.nextPlayer;
        data.nextPlayer = data.nextPlayer === 'X' ? 'O' : 'X';
      },
      to: ({data, from}) => winnerOf(data.board) || from,
    }}},
    xWon: {},
    oWon: {},
    draw: {},
  },
});
    },
  });
}]);
<button
  ng-repeat="cell in $ctrl.game.data.board"
  ng-disabled="!$ctrl.game.matches('playing') || cell !== '-'"
  ng-click="$ctrl.game.send('move', { index: $index })"
>
  {{ cell }}
</button>

<p ng-show="$ctrl.game.matches('playing')">
  Next: {{ $ctrl.game.data.nextPlayer }}
</p>

<p ng-show="$ctrl.game.data.winner">Winner: {{ $ctrl.game.data.winner }}</p>

The transition owns both validation and projection. Its to resolver predicts the next state without mutation; a valid move then updates data.board and whose turn is next. An invalid move records lastError in denied, leaving state unchanged. Machine transitions do not roll back data mutations when hooks throw.

Persist With Transition Hooks

Transition hooks are a convenient place to persist the game after each handled move. The hook runs after state has been updated, so the snapshot contains the terminal state when the move ends the game.

const persistedGame = $machine({
  ...gameConfig,
  hooks: {
    transition: ({ machine }) =>
      localStorage.setItem('game', JSON.stringify(machine.snapshot())),
  },
});

const saved = localStorage.getItem('game');
if (saved) persistedGame.restore(JSON.parse(saved));

restore() does not run hooks, so loading the saved game will not immediately write another snapshot. The next successful send() call will persist the updated state and data.

Snapshots

Use snapshot() to capture durable state and data. A snapshot is a structured clone of { state, data }:

const snapshot = session.snapshot();

localStorage.setItem('session', JSON.stringify(snapshot));

A snapshot contains only state and data. It has no version or machine id, and it does not include transitions or hooks; those still come from the machine config. Use $workflow or a workflow supervisor when persisted recovery needs versioning, diagnostics, history, retry, or migration.

The clone uses the browser’s structuredClone() behavior. Values such as objects, arrays, Dates, Maps, Sets, typed arrays, and cycles can be cloned. Functions, DOM nodes, and other non-cloneable values will throw the native structured clone error.

JSON persistence is a narrower contract. Use JSON.stringify(snapshot) only when data is JSON-compatible, or add your own encode/decode layer before storing the snapshot.

Use restore(snapshot) to recover an existing machine:

const snapshot = JSON.parse(localStorage.getItem('session'));

session.restore(snapshot);

restore() mutates the existing data object in place so scope proxies and templates keep their identity. Keys missing from the snapshot are removed, nested plain objects are merged in place, and non-plain values like arrays are replaced with cloned snapshot values. Restore accepts persisted input as unknown, validates its shape, and rejects a state that is not declared in states. Restore does not run transition hooks.

The configured initial state and every resolved transition target must also exist in states. Invalid built-in machine states fail at their boundary instead of leaving the machine in an unreachable state.

Hooks

Use hooks.enter and hooks.exit for state-specific effects. Use hooks.transition for logging or cross-cutting work that should run after any handled transition, including same-state transitions:

const session = $machine({
  initial: 'setup',
  data: { status: 'idle' },
  states: {
    setup: { on: { join: { to: 'waiting' } } },
    waiting: { on: { matched: { to: 'playing' } } },
    playing: {},
  },
  hooks: {
    exit: { setup: ({ data }) => (data.status = 'joining') },
    enter: { waiting: ({ data }) => (data.status = 'waiting') },
    transition: ({ type, from, to }) => console.log(type, from, to),
  },
});

Hook context contains type, from, to, payload, data, and machine. exit runs before state changes, enter runs after state changes, and transition runs after state-specific hooks. Missing transitions do not run hooks.

Hooks run synchronously inside the same send() batch. A hook may call machine.send() to run another valid transition; nested sends are batched with the outer transition. If a transition or hook throws, the error is rethrown, prior data/state mutations are kept, and AngularTS still schedules bound scopes for the keys touched by the attempted transition.

Scope Ownership

$machine(config) does not tie the machine to the lifetime of any one scope. The machine stores durable state and data internally. When an AngularTS scope proxy wraps the machine, the machine registers with that proxy and uses its scheduler for reactive template updates and batched send() calls.

That means a machine can be created once, assigned to a component, survive that component being destroyed, and later be assigned to another scope.

Prefer $machine(config) plus assignment to the controller, model, service, workflow, or runtime adapter that owns the machine. Named machines registered with module.machine() follow the same rule: they bind when a scope proxy observes them.

TypeScript

type SessionData = {
  roomId: string;
  error: string;
};

const session = $machine({
  initial: 'setup',
  data: {
    roomId: '',
    error: '',
  } satisfies SessionData,
  states: {
    setup: {
      on: {
        join: {
          to: 'waiting',
          update({ data }) {
            data.roomId = 'abc';
          },
        },
      },
    },
    waiting: {},
  },
});

session.send('join', { roomId: 'abc' });
session.send('reset');

Machine definitions are usable without generic parameters. Event names are inferred from the on maps and state names are inferred from states. Payloads remain unknown until an explicit event map supplies their types.

Add an event map when you want strict checked event names and payloads:

type SessionEvents = {
  join: { roomId: string };
  reset: undefined;
};

const session = $machine<SessionData, SessionEvents>({
  initial: 'setup',
  data: { roomId: '', error: '' },
  states: {
    setup: {
      on: {
        join: {
          to: 'waiting',
          update: ({ data, payload }) => (data.roomId = payload.roomId),
        },
      },
    },
    waiting: { on: { reset: { to: 'setup' } } },
  },
});

The ordinary authoring path does not require extraction helpers. Package authors can import MachineDataOf, MachineEventsOf, MachineEventNamesOf, and MachineStatesOf directly from @angular-wave/angular.ts/services/machine when an adapter needs to derive types from a reusable definition.

12 - $parse

URL normalization for HTML5/hashbang modes

13 - $rest

Typed REST resource client

The $rest service creates typed resource clients on top of a REST backend. Use this page for the usage model and examples. Exact class members, method signatures, return types, and configuration interfaces live in TypeDoc:

Creating A Resource

Inject $rest and call it with a base URL:

const posts = $rest<Post, number>('/api/posts');

The returned RestService supports common CRUD workflows:

const all = await posts.list();
const one = await posts.get(42);
const created = await posts.create({ title: 'Hello' } as Post);
const updated = await posts.update(42, { title: 'Updated' });
await posts.delete(42);

get(), create(), and update() return null when the backend returns no entity. delete() resolves with no value. All backend failures reject instead of being converted into fallback values.

Entity Mapping

Pass an entity class when raw JSON should be converted into richer objects.

class Post {
  id!: number;
  title!: string;
  createdAt: Date;

  constructor(raw: any) {
    Object.assign(this, raw);
    this.createdAt = new Date(raw.created_at);
  }
}

const posts = $rest<Post, number>('/api/posts', Post);
const post = await posts.get(1);

When an entity class is supplied, response objects are passed through new entityClass(data) before they are returned.

Request Options

The third factory argument is merged into backend requests. With the default HTTP backend, use it for headers, credentials, cache options, transforms, or custom param serialization.

const posts = $rest<Post, number>('/api/posts', Post, {
  headers: { 'X-Tenant': tenantId },
  withCredentials: true,
});

Pass backend when a resource should use a custom backend. The backend receives a normalized RestRequest with expanded URLs, params, request data, collection URL, and resource id. This is the extension point for tests, local persistence, IndexedDB, the browser Cache API, or composed network/cache behavior.

For cached reads, wrap the HTTP backend with CachedRestBackend:

const posts = $rest<Post, number>('/api/posts', Post, {
  backend: new CachedRestBackend({
    network: new HttpRestBackend($http),
    cache: appCache,
    strategy: 'network-first',
  }),
});

createRestCacheKey() is used internally by CachedRestBackend; it is not part of the top-level AngularTS namespace. Cache stores receive the final key string through the RestCacheStore methods and should treat it as opaque.

Request Bodies

When the default HTTP backend is used, request data is serialized by $http. Scope proxies are deproxied before JSON serialization, so proxy helper properties such as _target, _handler, and _proxy are not sent to the server. AngularTS-generated repeat identity is stored in internal metadata, not on the object, so it is not included in write payloads either.

Explicit application-owned fields are still normal data. If your model defines a property such as $hashKey, $rest does not remove it.

URI Templates

Resource URLs support RFC 6570 URI templates. Variables wrapped in braces are expanded from the params passed to methods such as list().

const repos = $rest<Repo>('/api/{org}/repos');

const angularRepos = await repos.list({
  org: 'angular-wave',
  type: 'public',
});

The org value expands into the path. Remaining params are forwarded to $http as query parameters.

Shared Defaults

Configure shared request defaults before bootstrap when a group of resources should use the same backend behavior.

angular.createModule('app', []).config({
  $rest: {
    defaults: {
      withCredentials: true,
      timeout: 5000,
    },
  },
});

Demo

The CRUD demo at /src/services/rest/rest-crud-demo.html talks to the Go demo backend through /api/tasks. It uses ng-repeat for rows, $rest for CRUD operations, and a cache strategy toggle for network-first, cache-first, and stale-while-revalidate.

14 - $rootElement

URL normalization for HTML5/hashbang modes

15 - $rootScope

Root scope service

16 - $security

Cross-cutting security policy gate for navigation and request-sensitive operations.

$security centralizes framework security policy decisions so apps can keep cross-cutting concerns out of every service call. The runtime-composed service allows by default and applies typed configuration from app.config({ $security: ... }).

Services consume this policy before sensitive side effects:

  • $http calls check() once and applies credentials from its allow decision.
  • $log synchronously gates Beacon delivery before serializing an entry.
  • Router transitions call check() before state changes proceed.

When no fallback is configured, the fallback decision is allow.

Exact policy signatures live in TypeDoc:

Configure

Use app.config({ $security: ... }) to set your app policy:

angular.createModule('app', []).config({
  $security: {
    fallback: 'deny',
    allowInsecureOrigins: ['http://localhost:3000'],
    isAuthenticated: function isAuthenticated() {
      return sessionStore.currentUser != null;
    },
    credentials: {
      bearer: function getToken() {
        return sessionStorage.getItem('token');
      },
      cookie: true,
      order: ['bearer', 'cookie'],
    },
    permissions: async function permissions(permission, context) {
      return permissionStore.allows(permission, context.to.name);
    },
  },
});

fallback controls unspecified behavior ("allow" | "deny"). Redirects are declared on route policies where a target is required.

Configuring a credential enables its request transport scheme. It does not declare that the current user is authenticated. isAuthenticated supplies that application state as a boolean or contextual async function. The optional credentials.order changes the default bearer, basic, then cookie precedence without requiring a second list of enabled schemes.

allowInsecureOrigins is an explicit allowlist for HTTP origins that may carry sensitive credentials. All other insecure origins remain blocked. Malformed origin strings are ignored, preserving fail-closed credential transport. Invalid policy enums and configuration shapes fail during setup.

credentials.bearer attaches a bearer token to allowed $http requests. Caller-provided Authorization headers are recognized automatically.

credentials.basic accepts username and password value sources and encodes the resulting credentials for the Authorization header.

credentials.cookie tells request services to use browser-managed cookies by enabling credentialed requests instead of manually writing a Cookie header.

Request policy contexts identify their framework transport. HTTP, Beacon, Worker, and Service Worker creation all pass through the same security gate. Beacon participates automatically, but its Web Platform API cannot apply custom headers. Bearer and Basic policies therefore deny Beacon delivery, while an enabled cookie session permits it. Security evaluation remains synchronous inside the Beacon transport so lifecycle delivery is never delayed by a Promise.

permissions receives each required permission and the current navigation context. It may return a boolean or a Promise. A static permission array is a shorthand for applications that do not need contextual authorization.

Route Policy

States can declare inherited navigation policy. The router merges policy from the target state path and passes the effective policy to $security before resolves, controllers, or views run.

angular
  .createModule('admin', [])
  .router({
    name: 'admin',
    url: '/admin',
    abstract: true,
    template: '<admin-layout ng-view></admin-layout>',
    policy: {
      navigation: {
        authenticated: true,
        permissions: ['admin:read'],
        redirectTo: 'login',
      },
    },
  })
  .router({
    name: 'admin.users',
    url: '/users',
    component: 'adminUsers',
  });

authenticated: true is satisfied only when the configured isAuthenticated evaluator resolves true. Credential transport and authentication state remain separate.

Required permissions are resolved by the configured permissions evaluator. If a required permission is missing, navigation is denied or redirected to the route policy’s redirectTo target.

Non-security route policies (loading, error, fallbackTo, retry) run in the same transition pipeline but do not replace authentication/authorization. They are complementary: $security decides whether navigation is allowed for the current principal, and route policies decide how the transition is rendered or recovered when that navigation remains allowed.

Use public: true on a child state to explicitly clear inherited navigation requirements. A public declaration cannot also specify authenticated, permissions, redirectTo, or reason because those fields would be contradictory or ignored.

Security policy has no public provider. Use app.config({ $security: ... }); route matching remains exclusively in the router state tree.

Executable config sample: security.html

Executable route policy sample: policy.html retry-fallback.html

17 - $serviceWorker

Service worker lifecycle, update, and message facade

$serviceWorker wraps browser-owned service worker registration, readiness, update observation, controller changes, and page-to-worker messaging.

Use it when the browser owns the page-control lifecycle. Use $worker for page-owned background computation. Use $rest and RestBackend for request and cache composition. Use $workflow for command execution and recovery; a service worker can be an activity boundary, not the workflow engine.

Exact method signatures live in TypeDoc:

Registration

Configure automatic registration on the module when the application has one stable worker script:

angular.createModule('app', []).serviceWorker('/sw.js', {
  scope: '/',
  updateViaCache: 'none',
  autoRegister: true,
});

Or configure the script without automatic registration and start it explicitly:

angular.createModule('app', []).serviceWorker('/sw.js', {
  scope: '/',
  updateViaCache: 'none',
});

$serviceWorker.register();

// Override browser registration options without repeating the configured URL.
$serviceWorker.register({ scope: '/feature/' });

$serviceWorker.ready().then(() => {
  status = $serviceWorker.status;
});

The default policy is observation only. AngularTS does not reload the page, activate a waiting worker, or pick a cache strategy for the app.

Destroying an AngularTS runtime removes its service-worker event listeners and reactive bindings. It does not call unregister(); browser registration remains active until the application explicitly unregisters it.

registrationState and updateState are readonly framework-owned views. Their properties update reactively as the browser registration changes.

Executable sample: service-worker.html

Update Prompt

Observe update state and make activation a user choice:

$serviceWorker.onUpdate((state) => {
  if (state.waiting) {
    showUpdatePrompt = true;
  }
});

If the user opts in, send your own protocol message to the waiting worker. Keep reload separate so the app can save state, finish work, or defer navigation.

function activateUpdate() {
  $serviceWorker.registration?.waiting?.postMessage({
    type: 'app:skip-waiting',
  });
}

$serviceWorker.onControllerChange(() => {
  showReloadButton = true;
});

function reloadAfterUserConfirms() {
  window.location.reload();
}

Messages

Use $serviceWorker.post(...) for simple page-to-worker messages after a controller is available:

$serviceWorker.onMessage((event) => {
  if (event.data.type === 'app:pong') {
    lastWorkerReply = event.data;
  }
});

$serviceWorker.post({
  type: 'app:ping',
}, {
  target: 'controller',
});

For request/response protocols, request() creates a dedicated MessageChannel, transfers its response port, and applies a timeout:

const profile = await $serviceWorker.request<Profile>(
  { type: 'profile:load' },
  { timeout: 5000 },
);

The service-worker message handler responds through event.ports[0]. AngularTS does not add request ids or prescribe an application message envelope.

Security

Registration participates in $security automatically. Bearer and Basic credential policies deny registration because the Web Platform registration API cannot attach application-defined Authorization headers. Cookie credentials remain browser-managed.

REST And Cache

Do not make $rest depend on service workers. REST caching belongs behind RestBackend and RestCacheStore; a service-worker-backed REST cache should be an explicit backend adapter when the app needs that boundary.

Unsupported Browsers

When navigator.serviceWorker is unavailable, $serviceWorker.supported is false, status is unsupported, and async lifecycle methods reject with a stable service-worker error. Applications can render a non-offline experience without special provider wiring.

18 - $templateCache

Map object for storing templates

$templateCache is a Map object used by template loading and component rendering.

Configure the cache before bootstrap with module.config({ $templateCache: ... }).

angular.createModule("demo", []).config({
  $templateCache: {
    cache: new Map([["templateId.html", "This is the content of the template"]]),
  },
});

Executable sample: template-cache.html

The first time a template is used, it is loaded in the template cache for quick retrieval. You can load templates directly into the cache in a script tag by using $templateRequest or by consuming the $templateCache service directly.

Adding via the script tag:

Example

<script type="text/ng-template" id="templateId.html">
  <p>This is the content of the template</p>
</script>

Note: the script tag containing the template does not need to be included in the head of the document, but it must be a descendent of the $rootElement (e.g. element with ng-app attribute), otherwise the template will be ignored.

Adding via the $templateCache service:

Example

const myApp = angular.createModule('myApp', []).run(["$templateCache", ($templateCache) => {
  $templateCache.set('templateId.html', 'This is the content of the template');
}]);

To retrieve the template, simply use it in your component:

myApp.component('myComponent', {
  templateUrl: 'templateId.html',
});

or include it with ng-include:

<div ng-include="'templateId.html`"></div>

or get it via the $templateCache service:

myApp.controller(
  'Test',
  ["$templateCache", class {
    constructor($templateCache) {
      const tmp = $templateCache.get('templateId.html');
    }
  }],
);

19 - $templateRequest

Template request service

$templateRequest fetches templates through $http and stores successful responses in $templateCache.

Configure template request defaults before bootstrap with module.config({ $templateRequest: ... }).

angular.createModule("demo", []).config({
  $templateRequest: {
    httpOptions: {
      headers: {
        "X-Template": "configured",
      },
      withCredentials: true,
    },
  },
});

Executable sample: template-request.html

Usage

$templateRequest("./panel.html").then((template) => {
  $templateCache.set("panel.html", template);
});

$templateRequest assumes template URLs are trusted by the caller. Security policy for resource URLs belongs to $compile.resourceUrlPolicy when templates are loaded through compiler bindings such as ng-include.

20 - $url

URL management for state transitions

21 - Binding policies

Destination-specific HTML, URL, and script binding policies

Binding policies

AngularTS uses synchronous callbacks configured through $compile to sanitize or validate values at their DOM destination. The former $sce and $sceDelegate services and their configuration keys have been removed.

Configuration

Each callback receives a string and must return a string or throw to reject it. Omitted callbacks retain the framework defaults.

Configuration callbackDestinationDefault
htmlPolicyng-bind-html and HTML propertiesReject nonempty strings
urlPolicyLink URLsBlock dangerous schemes
mediaUrlPolicyMedia URLs and each srcset candidateAllow supported media schemes and image data URLs
resourceUrlPolicyTemplates and embedded resourcesAllow same-origin HTTP(S) resources
scriptPolicyExecutable JavaScript sourceReject nonempty strings
scriptUrlPolicyScript-loading URLsAllow same-origin HTTP(S) resources
angular.createModule("app", []).config({
  $compile: {
    htmlPolicy: (html) => DOMPurify.sanitize(html),
    resourceUrlPolicy: (value) => {
      const url = new URL(value, document.baseURI);
      if (url.origin !== location.origin) {
        throw new Error("External resource rejected");
      }
      return value;
    },
  },
});

Supply your application’s sanitizer implementation, such as DOMPurify, before using this configuration. A policy callback is responsible for the values it approves. Policies are configured before bootstrap and are not injectable services.

Native TrustedHTML, TrustedScript, and TrustedScriptURL values are supported at their corresponding destinations. The framework does not create browser Trusted Types policies for the application.

Executable sample: sce.html.

22 - Workflow

Declarative asynchronous operation lifecycles with reactive state, cancellation, diagnostics, snapshots, and recovery.

Workflow

$workflow runs asynchronous operations through declarative lifecycles. A definition states where a command may run and what state and data changes occur while it is pending, after success, and after failure. Application code only requests the operation.

Use $machine for synchronous event-driven state. Use $workflow when an operation can wait, fail, time out, be cancelled, or require recovery.

Create A Workflow

app.workflow('checkout', {
  initial: 'ready',

  data: {
    receipt: null,
    failure: '',
  },

  commands: {
    submit: {
      from: 'ready',
      pending: 'processing',

      execute: ({ input, signal }) =>
        checkoutApi.submit(input, { signal }),

      success: {
        to: 'completed',
        update({ data, output }) {
          data.receipt = output;
          data.failure = '';
        },
      },

      failure: {
        to: 'failed',
        update({ data, diagnostic }) {
          data.failure = diagnostic.message;
        },
      },
    },
  },
});

Inject the named workflow and request the command:

app.controller('CheckoutCtrl', ["checkout", function (checkout) {
  this.checkout = checkout;

  this.submit = async (order) => {
    const result = await checkout.run('submit', order);

    if (!result.ok) {
      console.error(result.diagnostics);
    }
  };
}]);

Templates observe workflow state and data directly:

<button
  ng-click="$ctrl.submit($ctrl.order)"
  ng-disabled="!$ctrl.checkout.can('submit')"
>
  Submit
</button>

<p ng-if="$ctrl.checkout.state === 'processing'">Processing</p>
<p ng-if="$ctrl.checkout.state === 'failed'">
  {{ $ctrl.checkout.data.failure }}
</p>

Command Lifecycle

Every command declares:

  • from: state or states from which the command may run.
  • pending: state entered before execution starts.
  • execute: optional synchronous or asynchronous operation.
  • success: state entered when execution returns.
  • failure: state entered when execution throws or rejects.

pending, success, and failure accept either a state name or an object with to and update:

success: {
  to: 'completed',
  update({ data, input, output, command }) {
    data.receipt = output;
  },
}

Lifecycle updates are the writable boundary for workflow data. The data passed to execute is readonly, including nested plain objects, arrays, maps, and sets. This prevents a failed command from leaving untracked imperative changes.

Immediate Commands

An operation that only changes workflow state can omit execute:

commands: {
  reset: {
    from: ['completed', 'failed'],
    pending: 'resetting',
    success: {
      to: 'ready',
      update({ data }) {
        data.receipt = null;
        data.failure = '';
      },
    },
    failure: 'failed',
  },
}
await checkout.run('reset');

External approvals, WebSocket messages, user actions, and repair requests enter a workflow as commands. Workflows do not expose a separate event or send() API.

Results

run() always resolves to a WorkflowResult:

const result = await checkout.run('submit', order);

if (result.ok) {
  console.log(result.output);
} else if (result.status === 'timeout') {
  console.warn('The operation timed out.');
} else {
  console.warn(result.diagnostics);
}

Successful results have status completed. Failed results use failed, cancelled, timeout, or rejected.

rejected means execution did not fail unexpectedly. The command was unknown, was not allowed from the current state, violated its concurrency declaration, or called reject() with a controlled diagnostic.

Controlled Rejection

Use reject() for an expected business decision:

execute({ input, reject }) {
  if (!input.acceptedTerms) {
    return reject({
      code: 'checkout.termsRequired',
      message: 'Terms must be accepted.',
      recoverable: true,
    });
  }

  return checkoutApi.submit(input);
}

The framework records the diagnostic and applies the declared failure lifecycle.

Cancellation And Timeout

Commands receive an AbortSignal and may register cleanup callbacks:

execute({ input, signal, cleanup }) {
  const request = checkoutApi.submit(input, { signal });

  cleanup(() => request.releaseResources());

  return request;
}

Cancel one command or every running command:

checkout.cancel('submit');
checkout.cancel();

Declare timeout duration and outcome with the command:

submit: {
  from: 'ready',
  pending: 'processing',
  commandTimeout: 5000,
  execute: submitOrder,
  success: 'completed',
  failure: 'failed',
  cancelled: 'cancelled',
  timeout: 'timed-out',
}

When cancelled or timeout is omitted, the command uses its failure lifecycle.

Concurrency And Retry

Concurrency belongs to the declaration:

submit: {
  from: ['ready', 'processing'],
  pending: 'processing',
  concurrency: 'reject',
  retry: 2,
  execute: submitOrder,
  success: 'completed',
  failure: 'failed',
}
  • reject is the default and rejects another run of the same command.
  • queue executes runs of the same command in request order.
  • parallel allows overlapping execution when the declared source states also permit it.
  • retry is the number of automatic retries after unexpected execution failure.

Callers cannot override these properties through run(). Operational behavior therefore remains part of the workflow definition.

Diagnostics And History

workflow.diagnostics contains normalized, serializable evidence from failed operations. workflow.history records command start, completion, and final failure.

workflow.diagnostics;
workflow.history;

Use diagnosticLimit and historyLimit to bound retained entries:

app.workflow('checkout', {
  initial: 'ready',
  data: {},
  diagnosticLimit: 100,
  historyLimit: 500,
  commands: checkoutCommands,
});

Snapshot And Restore

Snapshots contain the workflow id, state, data, diagnostics, and history:

const snapshot = checkout.snapshot();

localStorage.setItem('checkout', JSON.stringify(snapshot));
checkout.restore(JSON.parse(localStorage.getItem('checkout')));

Use migrateSnapshot when persisted data predates the current snapshot shape:

migrateSnapshot(snapshot) {
  if (snapshot.version === 0) {
    return {
      version: 1,
      id: 'checkout',
      state: snapshot.stage,
      data: snapshot.data,
      diagnostics: [],
      history: [],
    };
  }

  return snapshot;
}

Supervisor

A workflow supervisor owns persistence and recovery for a named group:

app.workflowSupervisor('checkoutProcesses', {
  workflows: {
    checkout: checkoutDefinition,
    fulfillment: fulfillmentDefinition,
  },
  persistence: 'indexeddb',
  autoPersist: true,
  autoRecover: true,
});
app.controller('AdminCtrl', ["checkoutProcesses", function (checkoutProcesses) {
  this.checkout = checkoutProcesses.workflow('checkout');
}]);

Supervisors can also use a persistence adapter:

const persistence = {
  async load(id) {
    return database.load(id);
  },
  async save(id, snapshot) {
    await database.save(id, snapshot);
  },
};

autoPersist saves after every settled command. autoRecover restores the latest snapshot and reruns the latest recoverable failed command for each workflow.

Worker Runtime

The optional workflow worker runtime transports run, snapshot, and restore. It does not define another orchestration protocol:

const host = createWorkflowWorkerHost({
  workflows: { checkout },
});

const client = createWorkflowWorkerClient(workerConnection);
const result = await client.run('checkout', 'submit', order);

Runtime API

workflow.id;
workflow.state;
workflow.data;
workflow.diagnostics;
workflow.history;
workflow.can('submit');
workflow.run('submit', order);
workflow.cancel('submit');
workflow.snapshot();
workflow.restore(snapshot);

The runtime intentionally has no send(), manual lifecycle transition, or per-run orchestration options.