Standalone To-do walkthrough
Follow a service with a store, actions, events and cleanup in one runnable feature.
Build a small To-do feature with a service, actions and events. This example runs in Node.js with @bitakit/core and no framework.
It uses manual composition: definePlugin groups the files, and the record keys supply the same IDs discovery derives from file paths in an app. For the discovery-based workflow, start with the Counter learning path.
This walkthrough follows the packaged JavaScript example in @bitakit/core/examples/.
Use Node.js 24 or newer, install @bitakit/core, then copy the files while preserving their
relative paths:
cp -R node_modules/@bitakit/core/examples/. .The sections below explain those same files; you do not need to create a second copy. No React, router, database or discovery is required. For a TypeScript standalone example with derived service, Action and Settings maps, see Plugin manager.
The service owns todos in a store, three actions invoke it, the events file declares todos.changed, and the plugin groups them. The delete action is disabled while the list is empty because its enabled function reads the store.
todos/services/list.mjs
import { defineService } from '@bitakit/core';
/** The Todos data and behavior: an observable store plus the methods that change it. */
export default defineService(({ store, events }) => {
const state = store({ todos: [], nextId: 1 });
return {
state,
create(title) {
const todo = { id: state.get().nextId, title, done: false };
state.set((s) => ({ todos: [...s.todos, todo], nextId: s.nextId + 1 }));
events.emit('todos.changed', { kind: 'added', id: todo.id });
return todo;
},
all: () => [...state.get().todos],
delete(id) {
const before = state.get().todos.length;
state.set((s) => ({ ...s, todos: s.todos.filter((todo) => todo.id !== id) }));
const removed = state.get().todos.length < before;
if (removed) events.emit('todos.changed', { kind: 'removed', id });
return removed;
},
};
});todos/events.mjs
import { defineEvents, event } from '@bitakit/core';
export default defineEvents({
changed: event(),
});todos/actions/create.mjs
import { defineAction } from '@bitakit/core';
export default defineAction({
execute: ({ services, input }) => services.todos.list.create(input),
});todos/actions/all.mjs
import { defineAction } from '@bitakit/core';
export default defineAction({
execute: ({ services }) => services.todos.list.all(),
});todos/actions/delete.mjs
import { defineAction } from '@bitakit/core';
export default defineAction({
// Re-evaluated whenever the list store changes: disabled while the list is empty.
enabled: ({ services }) => services.todos.list.state.get().todos.length > 0,
execute: ({ services, input }) => services.todos.list.delete(input),
});todos/plugin.mjs
import { definePlugin } from '@bitakit/core';
import list from './services/list.mjs';
import events from './events.mjs';
import create from './actions/create.mjs';
import all from './actions/all.mjs';
import remove from './actions/delete.mjs';
export default definePlugin({
id: 'todos',
events,
services: { list }, // todos.list → services.todos.list
actions: { create, all, delete: remove }, // todos.create, todos.all, todos.delete
});todos.mjs
import { createRuntime } from '@bitakit/core';
import todos from './todos/plugin.mjs';
const runtime = createRuntime({ plugins: [todos] });
const { actions } = runtime;
const stop = runtime.mount();
try {
console.log('Delete enabled before:', actions.get('todos.delete').enabled);
await actions.execute('todos.create', 'Learn plugins');
await actions.execute('todos.create', 'Connect a service');
console.log('Delete enabled after:', actions.get('todos.delete').enabled);
console.log('Before:', (await actions.execute('todos.all')).value);
await actions.execute('todos.delete', 2);
console.log('After:', (await actions.execute('todos.all')).value);
} finally {
await stop();
}createRuntime(config, { actions, services }) creates the ActionList unless you pass one and accepts host-owned services. mount() returns the stop function; runtime.ready resolves after every service onStart.
Run and inspect
node todos.mjsThe delete action reports false before the first todo and true after it. The first list contains two todos; after deleting ID 2, the second contains only ID 1. Successful execution returns { status: 'completed', value: ... }. Failed execution rejects; inspect the execution outcome rather than assuming every call ran.
The store is temporary memory. Events notify subscribers after a mutation; they do not hold the todo data. Removing a todo does not unregister the delete action. Runtime cleanup releases registrations, stores and owned subscriptions.
Try deleting every todo: the enabled function makes the delete action unavailable once the store is empty. A real product must still validate inputs and authorize mutations on the server.