---
title: "Create a node in TypeScript"
description: "Scaffold, implement, test, and push a TypeScript node with the Axiom CLI, using google-protobuf message classes and AxiomContext."
category: guide
surfaces: [cli, sdk]
languages: [typescript]
related: [getting-started/first-node, getting-started/first-flow, concepts/nodes-packages-flows, concepts/type-system, reference/sdk/typescript, guides/manage-secrets, reference/axiom-yaml]
last_reviewed: 2026-06-06
---

# Create a node in TypeScript

A node is a plain TypeScript function: it takes `AxiomContext` and a typed
input message, and returns a typed output message. This guide scaffolds a
TypeScript package, implements and tests a node, runs it locally with hot
reload, and pushes it to the platform.

## Prerequisites

- The Axiom CLI installed and logged in (`axiom login`) — see
  [Installation](../getting-started/installation.md).
- Node.js and npm.
- `protoc` on your PATH ([installation guide](https://grpc.io/docs/protoc-installation/)),
  plus the two protoc plugins TypeScript codegen uses:

```bash
# protoc-gen-ts comes from ts-protoc-gen; protoc-gen-js emits the JS message runtime
npm install -g ts-protoc-gen protoc-gen-js
```

`axiom generate` compiles `messages/*.proto` with `protoc-gen-js` (the
`google-protobuf` runtime classes, `gen/messages_pb.js`) and `protoc-gen-ts`
(the matching type declarations, `gen/messages_pb.d.ts`). It reports which
tool is missing if one is not found.

## Scaffold the package and node

Run these commands to go from nothing to a compiling node skeleton:

```bash
# from any working directory
axiom init my-org/greeter --language typescript
cd greeter
npm install

axiom create message GreetRequest --fields "name:string"
axiom create message GreetReply --fields "greeting:string"
axiom create node Greet --input GreetRequest --output GreetReply
```

`axiom init` creates a `greeter/` directory (the part of the package name
after the last `/`) containing `axiom.yaml`, `messages/`, `nodes/`, `gen/`,
a `.gitignore`, and the TypeScript toolchain files: `package.json` (with
`google-protobuf`, the `@grpc/*` stack, and `jest`/`ts-jest` dev
dependencies), `tsconfig.json`, and `jest.config.js`.

`axiom create message` appends a message block to
`messages/messages.proto` — all messages live in that single file. The
`--fields` flag accepts colon shorthand (`name:string`) or canonical proto3;
omit it to get example placeholder fields (plus a syntax-hints comment
block) to replace. Comments written directly above a message or field (no
blank line between) are extracted at publish time as registry
documentation.

`axiom create node` requires a PascalCase name and creates
`nodes/greet.ts` (the handler, with a camelCase function name `greet`) and
`nodes/greet_test.ts` (a jest test with a ready-made mock `AxiomContext`),
then records the node in `axiom.yaml`:

```yaml
# axiom.yaml (written by init + create node)
name: my-org/greeter
version: 0.1.0
language: typescript
nodes:
    - name: Greet
      input: GreetRequest
      output: GreetReply
```

After the scaffold sequence the generated files are already in place:
`axiom create message` runs `axiom generate` automatically (compiling
`gen/messages_pb.js` and `gen/messages_pb.d.ts`), and `axiom create node`
writes `gen/axiomContext.ts`. Both commands accept `--no-generate` to skip
their generate step while batching; run `axiom generate` afterwards to
produce all three files at once. For TypeScript packages, `gen/` is
committed to git; `.axiom/` (build artifacts), `node_modules/`, and `dist/`
are ignored. See [The type system](../concepts/type-system.md)
for how messages define your node's contract.

## Implement the handler

Edit the generated file. Input and output are `google-protobuf` classes with
getter/setter accessors:

```typescript
// nodes/greet.ts
import { GreetRequest, GreetReply } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';

/**
 * Greets the caller by name. This JSDoc directly above the function is
 * extracted at publish time and shown in the Axiom registry as the node's
 * description — edit it before publishing.
 *
 * @param ax - Platform context: ax.log for logging, ax.secrets for secrets.
 */
export function greet(ax: AxiomContext, input: GreetRequest): GreetReply {
  ax.log.info('greeting', { name: input.getName() });
  const out = new GreetReply();
  out.setGreeting(`Hello, ${input.getName()}!`);
  return out;
}
```

Handlers may also be `async` — declare the return type as
`Promise<GreetReply>` and the platform awaits the result. This is required
when you use the Promise-based memory API
(`await ax.agent.memory.session(id).history().last(20)`).

## Use platform capabilities through AxiomContext

Every platform capability arrives through the first parameter, `ax` — node
code never calls platform services directly (see
[Sandboxing and tenancy](../concepts/sandboxing-and-tenancy.md)):

- `ax.log.debug/info/warn/error(msg, attrs?)` — structured logging. Use it
  instead of `console.log()`: in production every line carries the execution
  ID and trace IDs.
- `ax.secrets.get(name)` — returns a `[value, ok]` tuple:

```typescript
// nodes/greet.ts — inside the handler
const [apiKey, ok] = ax.secrets.get('OPENAI_API_KEY');
if (!ok) {
  ax.log.warn('OPENAI_API_KEY is not configured');
}
```

  Declare each secret a node reads under `required_secrets` in `axiom.yaml`
  so it is validated at publish time and shown in the marketplace — see
  [Manage secrets in a flow](manage-secrets.md):

```yaml
# axiom.yaml — node entry with a declared secret
nodes:
    - name: Greet
      input: GreetRequest
      output: GreetReply
      required_secrets:
        - OPENAI_API_KEY
```

- `ax.agent.memory` — session history and agent memory
  (see [Agent memory](../concepts/memory.md)).
- `ax.executionId`, `ax.flowId`, `ax.tenantId` — identifiers for the current
  invocation.

The full interface is documented in the
[TypeScript SDK reference](../reference/sdk/typescript.md).

## Test the node

`axiom test` runs three steps: `axiom generate`, `axiom validate`
(manifest schema, proto compilation, node signatures — fails fast), then
jest (preferring the locally installed `node_modules/.bin/jest`). Tests live
in `nodes/*_test.ts`; the scaffolded `nodes/greet_test.ts` already defines a
mock `testContext` you can pass to the handler — replace its TODO assertion
with real ones:

```typescript
// nodes/greet_test.ts — replace the generated TODO with real assertions
describe('Greet', () => {
  it('greets by name', () => {
    const input = new GreetRequest();
    input.setName('Ada');
    const result = greet(testContext, input);
    expect(result).toBeInstanceOf(GreetReply);
    expect(result.getGreeting()).toBe('Hello, Ada!');
  });
});
```

```bash
axiom test                              # full suite
axiom test -- --testNamePattern Greet   # pass-through args after --
```

`axiom validate` warns — without blocking — when a node has no test, and
`axiom test` runs the suite with jest. The push build compiles the service
but does not run tests, so a green `axiom test` before pushing is the
quality gate that matters. Assert output fields meaningfully — not just
type-checked.

## Run the node locally

`axiom dev` generates the gRPC service, runs it with `npx ts-node`, and
starts an HTTP bridge (default port 8083, change with `--port`) that
converts JSON to and from Protocol Buffers. It watches `nodes/`,
`messages/`, and `axiom.yaml` and recompiles on change; a failed build
leaves the previous service running while you fix the error.

```bash
axiom dev
```

Then, in another terminal:

```bash
# invoke the node over the HTTP bridge
curl localhost:8083/nodes/Greet -d '{"name":"Ada"}'
# → {"greeting":"Hello, Ada!"}
```

The bridge also serves `GET /openapi.json` (importable into Postman,
Insomnia, or Bruno) and an interactive try-it page at `GET /docs`.

## Push to the platform

```bash
axiom push
```

`axiom push` validates the package locally, then pushes it. A pushed package
is visible only to your own tenant — it does not appear in the public
marketplace, and you can push the same version repeatedly while iterating.
It requires a prior `axiom login`, and your current git HEAD must be pushed
to the remote (the platform builds from it). When you are satisfied, publish
through the Axiom UI to make an immutable public release. Next: place the
node in a flow — see [Your first flow](../getting-started/first-flow.md).

## Streaming nodes (pipeline)

For streaming, scaffold a pipeline node with `--type pipeline`:

```bash
axiom create node Tokenize --input GreetRequest --output GreetReply --type pipeline
```

The handler is an async generator: it consumes an `AsyncIterable` of input
frames and yields output frames. When a pipeline node is the entry node of a
flow, the iterable yields exactly one item.

```typescript
// nodes/tokenize.ts
import { GreetRequest, GreetReply } from '../gen/messages_pb';
import { AxiomContext } from '../gen/axiomContext';

/**
 * Emits one greeting frame per input frame.
 */
export async function* tokenize(
  ax: AxiomContext,
  inputs: AsyncIterable<GreetRequest>,
): AsyncGenerator<GreetReply> {
  for await (const input of inputs) {
    const out = new GreetReply();
    out.setGreeting(`Hello, ${input.getName()}!`);
    yield out;
  }
}
```

Flows containing pipeline nodes run in pipeline mode — see
[Execution model](../concepts/execution-model.md).
