Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Principles
Tooling & Workspace
Workspace Overview
Cli
Jobs & Commands
SDK
Error Handling
Extensions
Typescript
Go
Python
Docker
Ci
Frameworks
Typescript
OverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart Client
Go
OverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Platform
  1. DocsSeparator
  2. Tooling & WorkspaceSeparator
  3. Extensions

Extensions

Extensions are packages that provide job implementations and capabilities to your Putnami workspace. They are the mechanism by which Putnami supports different languages, frameworks, and tooling integration.

Extensions are auto-discovered from your workspace dependencies and workspace packages. Any package with a putnami.extension.json file is automatically registered as an extension.

Install extensions

Install extensions at the workspace root:

bun add @putnami/typescript @putnami/go @putnami/python @putnami/git

Extensions are discovered automatically from:

  1. Local paths listed in .putnamirc.json extensions field (for local-path extensions)
  2. dependencies in the root package.json
  3. devDependencies in the root package.json
  4. workspaces entries (for local development)

npm-based extensions require no manual configuration. Extensions in any language are registered via .putnamirc.json — see Any-Language Extensions.

Extension caching

To optimize startup time, discovered extensions are cached in .putnami/extensions.cache.json. The cache is automatically invalidated when dependencies change (for example after putnami deps install, putnami deps add, or putnami deps remove).

You can manually reset the cache with:

bunx putnami extensions reset

Disabling extensions

To disable specific extensions, add them to disable.extensions in the putnami field of the root package.json:

{
  "name": "my-workspace",
  "putnami": {
    "disable": {
      "extensions": ["@putnami/go"]
    }
  }
}

Disabling specific jobs

If you only need to disable specific jobs (rather than an entire extension), use disable.jobs in the putnami field of the root package.json:

{
  "name": "my-workspace",
  "putnami": {
    "disable": {
      "jobs": ["@putnami/typescript:lint"]
    }
  }
}

Or disable jobs per-project in the project's package.json:

{
  "name": "@myorg/my-app",
  "putnami": {
    "disable": {
      "jobs": ["lint"]
    }
  }
}

Unqualified names (e.g., "lint") disable all jobs with that name. Qualified names (e.g., "@putnami/typescript:lint") disable only the specific extension's job.

See Jobs & Commands for details.

Extension dependencies

Extensions can declare dependencies on other extensions with optional version constraints:

{
  "extensionDependencies": ["@putnami/typescript"]
}

Or with version constraints:

{
  "extensionDependencies": {
    "@putnami/typescript": "^2.0.0",
    "@putnami/python": ">=1.5.0"
  }
}

Supported version patterns:

  • * — Any version

  • workspace:* — Any workspace version

  • ^1.2.3 — Compatible with 1.2.3 (semver caret)

  • ~1.2.3 — Approximately 1.2.3 (semver tilde)

  • >=1.0.0 — Greater than or equal

  • 1.2.3 — Exact version

  • Dependencies are validated when extensions are loaded

  • Version mismatches throw a descriptive error

  • Circular dependencies are detected and throw an error

  • Missing dependencies throw a descriptive error

Job priority

When multiple extensions provide the same job name, priority determines which one runs:

{
  "jobs": {
    "build": {
      "kind": "command",
      "command": "bun",
      "args": ["@my/extension:build"],
      "priority": 10
    }
  }
}
  • Higher priority value wins (default: 0)
  • Only the highest-priority matching job runs for each project

Lifecycle hooks

Extensions can define lifecycle hooks for cross-cutting concerns like logging, metrics, or cleanup.

Module hooks (legacy)

Module hooks import JavaScript modules directly:

{
  "hooks": {
    "onLoad": "./hooks/on-load",
    "onBeforeJob": "./hooks/on-before-job",
    "onAfterJob": "./hooks/on-after-job"
  }
}

Available hooks:

  • onLoad — Called when the extension is first loaded
  • onBeforeJob — Called before each job execution
  • onAfterJob — Called after each job completes (success or failure)

Hook implementation:

// hooks/on-before-job.ts
import type { OnBeforeJobContext } from '@putnami/sdk/extensions';

export const hook = async (ctx: OnBeforeJobContext): Promise<void> => {
  console.log(`Starting job ${ctx.jobName}`);
};

Hook failures are logged but do not fail the job execution.

Command hooks

Command hooks execute as subprocesses, enabling language-agnostic implementations and better isolation:

{
  "hooks": {
    "preBuild": {
      "kind": "command",
      "command": "bunx",
      "args": ["@my/extension:generate"],
      "cwd": "{projectRoot}",
      "timeoutMs": 120000,
      "cache": {
        "inputs": ["src/**/*.tsx"],
        "outputs": [".gen/**/*"]
      }
    }
  }
}

Command hook properties:

Property Type Description
kind "command" Discriminator for command hooks
command string Command to execute (e.g., bunx, node, python)
args string[] Arguments to pass
cwd string? Working directory (supports template variables)
env Record<string, string>? Environment variables
timeoutMs number? Timeout in milliseconds (default: 120000)
cache object? Cache configuration for incremental builds

Template variables:

  • {workspaceRoot} — Absolute path to workspace root
  • {projectRoot} — Absolute path to project root
  • {extensionRoot} — Absolute path to extension package
  • {outputRoot} — Output directory (usually .gen)
  • {cacheRoot} — Cache directory for the extension

JSONL output protocol:

Command hooks communicate via JSON Lines on stdout. The SDK provides helpers for TypeScript:

#!/usr/bin/env bun
import {
  CommandModel,
  Flag,
  runHookCommand,
  emitLog,
  emitProgress,
  type HookContext,
  type HookResult,
  type StandardHookOptions,
} from '@putnami/sdk';

@CommandModel({ name: 'generate', description: 'Generate code' })
class GenerateOptions implements StandardHookOptions {
  @Flag({ flagName: '--putnami-context', required: true })
  putnamiContext = '';

  @Flag({ flagName: '--output' })
  output: 'text' | 'jsonl' | 'both' = 'jsonl';

  @Flag({ flagName: '--help', shorts: ['-h'], required: false })
  help = false;
}

async function runGenerate(
  _options: GenerateOptions,
  context: HookContext
): Promise<HookResult> {
  emitLog('info', `Working in ${context.projectRoot}`);
  emitProgress('Generating', 50, 'codegen');

  // ... generation logic ...

  return {
    exports: { loader: '/path/to/loader.ts' },
    assets: { 'bundle.js': '/path/to/bundle.js' },
  };
}

if (import.meta.main) {
  runHookCommand({
    model: GenerateOptions,
    extension: '@my/extension',
    hook: 'preBuild',
    version: '1.0.0',
    run: runGenerate,
  });
}

Exit codes:

Code Meaning
0 Success
1 Build error
2 Invalid configuration
130 Interrupted (SIGINT)

Official Extensions

@putnami/typescript

The primary extension for JavaScript and TypeScript development. It handles building, testing, and linting for JS/TS projects.

Purpose:

  • Provides a unified toolchain for TypeScript development.
  • Eliminates the need for per-package build configuration files.

Jobs:

  • build: Bundles code using a combination of Bun and Esbuild for optimal performance. Supports browser, node, and serviceworker targets.
  • test: Runs tests using the highly performant Bun test runner.
  • lint: Orchestrates ESLint and Prettier to ensure code quality and formatting.

Templates: typescript-web, typescript-server, typescript-library

Configuration:

  • coverage (boolean): Enable test coverage reporting (default: true).
  • fix (boolean): Auto-fix linting issues where possible (default: true).

@putnami/go

Provides comprehensive support for the Go programming language. All job scripts are standalone shell scripts — no Bun or npm required at runtime.

Purpose:

  • Abstraction over standard Go commands for workspace compatibility.
  • Works as an any-language extension with the compiled CLI binary.

Jobs:

  • build: Compiles Go binaries and libraries using go build.
  • test: Runs go test with race detection and coverage.
  • lint: Runs golangci-lint and staticcheck for static analysis.
  • serve: Runs Go applications in development or production mode.

Templates: go-server, go-library

Self-contained: Fully self-contained — Go is auto-downloaded on first use. Only bash, jq, and curl/wget needed. See Go getting started.

@putnami/python

Enables Python support within the workspace, facilitating hybrid applications.

Purpose:

  • Integrate Python services or scripts alongside other code.
  • Standardize Python tooling execution.

Jobs:

  • build: Prepares Python packages.
  • test: Runs Python tests (e.g., using pytest).
  • lint: Runs Python linters (e.g., ruff or pylint).

Templates: python-server, python-library

@putnami/git

Provides Git integration for versioning and repository management.

Purpose:

  • Automate version tagging and changelog generation.
  • Enforce commit message conventions via hooks.

Jobs:

  • tag: Creates git tags for released versions.
  • versioning: Manages semantic versioning bumping.

Any-Language Extensions

Extensions can be written in any language — Go, Python, shell scripts, Rust, etc. — without requiring npm, TypeScript, or Bun. These extensions declare their CLI flags in the manifest and use any executable as the job command. Combined with the compiled CLI binary, this enables fully npm-free deployments.

Step-by-step tutorial: How to: Create an extension

Structure

my-extension/
├── putnami.extension.json    # Manifest with embedded flags
└── bin/
    ├── build                 # Executable (any language)
    ├── test                  # Executable
    └── lint                  # Executable

No package.json, no node_modules, no TypeScript modules required.

Manifest with embedded flags

Declare CLI flags directly in putnami.extension.json:

{
  "name": "my-go-extension",
  "jobs": {
    "build": {
      "kind": "command",
      "command": "{extensionRoot}/bin/build",
      "cwd": "{projectRoot}",
      "filePatterns": ["**/*.go", "go.mod"],
      "flags": {
        "target": { "type": "string", "default": "linux/amd64", "description": "Build target" },
        "race": { "type": "boolean", "default": false, "description": "Enable race detector", "short": "-r" },
        "tags": { "type": "array", "description": "Build tags" }
      }
    }
  }
}

Flag definition properties:

Property Type Required Description
type "string" | "boolean" | "number" | "array" Yes Value type. "array" allows repeated --flag v1 --flag v2
short string No Short alias (e.g., "-r")
description string No Help text
default string | boolean | number | string[] No Default value
required boolean No Whether the flag must be provided
choices string[] No Valid values for "string" or "array" types

When flags are present in a job definition, the SDK builds CLI commands directly from them instead of importing a TypeScript module.

Template variables

These placeholders are resolved at runtime in command, args, cwd, and env values:

Variable Description
{workspaceRoot} Absolute path to the workspace root
{projectRoot} Absolute path to the project being processed
{extensionRoot} Absolute path to the extension directory
{outputRoot} Output directory for job artifacts
{cacheRoot} Cache directory for the extension

Registering extensions via local paths

Add extension paths to .putnamirc.json at the workspace root:

{
  "extensions": [
    "./my-extensions/go-ext",
    "/opt/shared-extensions/custom-ext"
  ]
}

Paths can be relative (resolved from workspace root) or absolute. Each path must contain a putnami.extension.json manifest.

The extension name is resolved from (in order):

  1. The name field in putnami.extension.json
  2. The name field in package.json (if present)
  3. The directory name as fallback

JSONL event protocol

Extensions communicate with the Putnami orchestrator via JSONL events on stdout. Each line is a JSON object with protocol version v: 1 and a type field.

Important: stdout is reserved for JSONL events. Use stderr for human-readable diagnostic output.

Invocation

The orchestrator spawns the job command as:

{command} [args...] --putnamiContext <file> --output jsonl [job-flags...]

The --putnamiContext argument points to a temporary JSON file containing workspace, project, extension, and parameter context:

{
  "workspaceRoot": "/home/user/my-workspace",
  "workspace": { "name": "my-workspace", "rootPath": "/home/user/my-workspace" },
  "project": {
    "name": "@myorg/my-app",
    "path": "apps/my-app",
    "fullPath": "/home/user/my-workspace/apps/my-app"
  },
  "extension": { "name": "my-extension", "path": "./extensions/my-extension" },
  "job": { "name": "build" },
  "params": { "target": "linux/amd64", "race": false },
  "outputPath": "/tmp/putnami/my-extension/my-app/build",
  "cacheRoot": "/tmp/putnami/my-extension/my-app"
}

The project field is absent for workspace-level jobs.

Event types

Event Description Required fields
meta Job metadata (emit once at start) data: { extension, job }
log Structured log message level, message
phase Lifecycle phase markers name, action (start/end), status on end
progress Progress reporting current, optionally total and message
diagnostic Errors/warnings with source location severity, message, optionally location: { file, line }
metric Performance and size metrics name, value, unit
artifact Output artifact registration id, name, kind, path
result Job result (must be the last event) data: { status: "OK" | "FAILED" | "SKIP" }

Example JSONL output

{"v": 1, "type": "meta", "time": "...", "level": "info", "message": "Starting build", "data": {"extension": "my-ext", "job": "build"}}
{"v": 1, "type": "phase", "time": "...", "name": "compile", "action": "start"}
{"v": 1, "type": "log", "time": "...", "level": "info", "message": "Compiling 42 files..."}
{"v": 1, "type": "progress", "time": "...", "current": 21, "total": 42, "message": "Compiling..."}
{"v": 1, "type": "diagnostic", "time": "...", "severity": "warning", "message": "unused import", "location": {"file": "src/main.go", "line": 5}}
{"v": 1, "type": "metric", "time": "...", "name": "binary-size", "value": 4096, "unit": "bytes"}
{"v": 1, "type": "artifact", "time": "...", "id": "binary", "name": "App Binary", "kind": "file", "path": "bin/app"}
{"v": 1, "type": "phase", "time": "...", "name": "compile", "action": "end", "status": "success"}
{"v": 1, "type": "result", "time": "...", "level": "info", "message": "Job OK", "data": {"status": "OK"}}

Exit codes

Code Meaning
0 Success (status: "OK" or "SKIP")
1 Build error (status: "FAILED")
2 Invalid config (missing context file, bad parameters)
130 Interrupted (SIGINT)

Example: Shell script extension

A complete extension using shell scripts. The putnami-jsonl.sh helper library provides emit functions for all event types.

putnami.extension.json:

{
  "name": "shell-builder",
  "jobs": {
    "build": {
      "kind": "command",
      "command": "{extensionRoot}/bin/build",
      "cwd": "{projectRoot}",
      "flags": {
        "target": { "type": "string", "default": "release", "choices": ["debug", "release"] },
        "clean": { "type": "boolean", "default": false, "description": "Clean before building" }
      }
    }
  }
}

bin/build:

#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "$0")/putnami-jsonl.sh"

# Parse arguments.
CONTEXT_FILE="" TARGET="release" CLEAN="false"
while [[ $# -gt 0 ]]; do
  case "$1" in
    --putnamiContext) CONTEXT_FILE="$2"; shift 2 ;;
    --output)        shift 2 ;;
    --target)        TARGET="$2"; shift 2 ;;
    --clean)         CLEAN="true"; shift ;;
    --no-clean)      CLEAN="false"; shift ;;
    *)               shift ;;
  esac
done

[ -z "$CONTEXT_FILE" ] && { echo "Error: --putnamiContext required" >&2; exit 2; }

# Read context (sets PUTNAMI_CTX_* variables).
parse_context "$CONTEXT_FILE"

emit_meta "$PUTNAMI_CTX_EXTENSION_NAME" "$PUTNAMI_CTX_JOB_NAME"
emit_phase_start "build"
emit_log "info" "Building $PUTNAMI_CTX_PROJECT_NAME (target=$TARGET)..."

if make -C "$PUTNAMI_CTX_PROJECT_PATH" "$TARGET" 2>/dev/stderr; then
  emit_phase_end "build" "success"
  emit_result "OK"
  exit 0
else
  emit_phase_end "build" "failed"
  emit_result "FAILED"
  exit 1
fi

A reusable putnami-jsonl.sh helper library and complete sample extension are available in samples/shell-extension/.

Example: Python extension

bin/test (Python):

#!/usr/bin/env python3
import argparse, json, subprocess, sys
from datetime import datetime, timezone

def emit(event):
    event.setdefault("v", 1)
    event.setdefault("time", datetime.now(timezone.utc).isoformat())
    print(json.dumps(event), flush=True)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--putnamiContext", required=True)
    parser.add_argument("--output", default="jsonl")
    parser.add_argument("--verbose", "-V", action="store_true")
    args = parser.parse_args()

    with open(args.putnamiContext) as f:
        ctx = json.load(f)

    project = ctx.get("project")
    if not project:
        emit({"type": "result", "data": {"status": "SKIP"}})
        sys.exit(0)

    emit({"type": "meta", "level": "info", "message": "Starting test",
          "data": {"extension": ctx["extension"]["name"], "job": "test"}})
    emit({"type": "phase", "name": "test", "action": "start"})

    result = subprocess.run(
        ["python", "-m", "pytest", "-v" if args.verbose else "-q"],
        cwd=project["fullPath"], capture_output=True, text=True,
    )

    if result.returncode == 0:
        emit({"type": "phase", "name": "test", "action": "end", "status": "success"})
        emit({"type": "result", "level": "info", "message": "Job OK", "data": {"status": "OK"}})
    else:
        for line in result.stdout.splitlines():
            if "FAILED" in line:
                emit({"type": "diagnostic", "severity": "error", "message": line.strip()})
        emit({"type": "phase", "name": "test", "action": "end", "status": "failed"})
        emit({"type": "result", "level": "info", "message": "Job FAILED", "data": {"status": "FAILED"}})
        sys.exit(1)

if __name__ == "__main__":
    main()

Creating Custom Extensions

You can extend Putnami's capabilities by creating your own extensions. An extension is simply a package that provides job implementations — either as TypeScript modules (npm-based) or as executables in any language. For non-TypeScript extensions, see the how-to guide for a step-by-step tutorial.

To create an extension:

  1. Create a new package (workspace package or published npm package).
  2. Add a putnami.extension.json describing your commands and jobs.
  3. Export the job implementation files referenced by putnami.extension.json.
  4. Install the extension at the workspace root.

Minimal putnami.extension.json

{
  "extensionDependencies": {
    "@putnami/typescript": "^1.0.0"
  },
  "hooks": {
    "preBuild": {
      "kind": "command",
      "command": "bun",
      "args": ["@my/extension:pre-build"],
      "cwd": "{projectRoot}"
    }
  },
  "commands": {
    "build": "./build"
  },
  "jobs": {
    "build": {
      "kind": "command",
      "command": "bun",
      "args": ["@my/extension:build"],
      "cwd": "{projectRoot}",
      "filePatterns": ["src/**/*", "package.json"],
      "priority": 0
    }
  }
}

Extension properties:

  • extensionDependencies (optional): Other extensions this extension depends on (array or object with version constraints)
  • hooks (optional): Command hooks for build lifecycle (e.g., preBuild)
  • commands: CLI command mappings to module paths
  • jobs: Command-based job definitions
  • autoServe (optional, default: true): Whether projects with this extension should be auto-discovered as service dependencies during putnami serve. Set to false to prevent infrastructure packages from being auto-started in multi-service sessions. Projects can still be served explicitly or via runsWith.

Job properties:

  • kind: "command" — discriminator for command-based jobs
  • command: Command to execute (e.g., "bun", "node")
  • args: Arguments to pass to the command
  • cwd (optional): Working directory (supports template variables)
  • dependsOn (optional): Job dependencies using scope syntax (for project-level overrides; in manifests, use pipeline step dependsOn instead)
  • filePatterns (optional): Files to track for cache invalidation
  • priority (optional): Priority for job selection (higher wins, default: 0)
  • cache (optional): Whether to cache results (default: false)
  • channel (optional): Publish channel (e.g., "npm", "docker"). When set, the job only runs if the project's putnami.publish includes this channel

dependsOn scope syntax

Use prefixes in pipeline step dependsOn to control where a dependency runs. Steps can mix intra-pipeline refs (step ids) with external refs:

  • "stepId" — intra-pipeline step dependency
  • "^stepId" — individual task on all transitive upstream dependencies (when the ref matches a step in the current pipeline)
  • "^commandName" — full command on upstream dependencies (when the ref does NOT match a step in the current pipeline)
  • "*job" — job on all downstream dependents
  • "/job" — workspace-level job (no project context)

Any external ref can be scoped to a specific extension using @extension/name:job syntax:

  • "^@putnami/typescript:publish" — only the typescript extension's publish on upstream dependencies

This is useful when multiple extensions provide the same job name and you need to control which one cascades through the dependency graph.

^stepId (task-level upstream deps): When the ^ ref matches a step in the current pipeline, the planner emits only that individual task for each transitive upstream project — not their full pipeline. Dependencies between upstream tasks follow the project dependency graph and the pipeline's intra-step ordering.

{
  "commands": {
    "publish": {
      "run": [
        { "id": "generate", "task": "build-generate", "dependsOn": ["^generate"] },
        { "id": "compile", "task": "build-compile", "dependsOn": ["generate", "^compile"] },
        { "id": "docker", "task": "publish-docker", "dependsOn": ["compile"], "if": "params.docker" }
      ]
    }
  }
}

Running putnami publish my-app (where my-app depends on sdk which depends on utils):

  • utils and sdk get only generate and compile tasks
  • my-app gets the full pipeline (generate, compile, docker)
  • No upstream npm/docker/sentinel jobs are scheduled

^commandName (full command fallback): When the ^ ref does NOT match any step in the current pipeline, the planner schedules the full command on upstream projects. This is used for cross-command dependencies.

{
  "commands": {
    "build": {
      "run": [
        { "id": "tidy", "task": "go-tidy" },
        { "id": "compile", "task": "go-compile", "dependsOn": ["tidy", "^build"] }
      ]
    }
  }
}

Flags can be appended to any external ref — prefixed references (^, *, /) and extension-scoped references. The planner splits each string on whitespace: the first token is the job reference, and the rest are forwarded as arguments.

When multiple steps schedule the same dependency with different flags, the planner upgrades to an unrestricted execution (no flags) so the result satisfies all dependents.

Job CLI script

Jobs are implemented as CLI scripts executed as subprocesses:

#!/usr/bin/env bun
// bin/build.ts
import {
  runJobCommandCli,
  emitLog,
  type JobCommandContext,
  type JobResult,
  type StandardJobOptions,
  CommandModel,
  Flag,
} from '@putnami/sdk';

@CommandModel({ name: 'build', description: 'Build project' })
class BuildOptions implements StandardJobOptions {
  @Flag({ flagName: '--putnamiContext', required: true })
  putnamiContext = '';

  @Flag({ flagName: '--output' })
  output: 'text' | 'jsonl' | 'both' = 'jsonl';

  @Flag({ flagName: '--help', shorts: ['-h'], required: false })
  help = false;
}

async function runBuild(
  _options: BuildOptions,
  context: JobCommandContext
): Promise<JobResult> {
  if (!context.project) {
    return { status: 'SKIP' };
  }
  emitLog('info', `Building ${context.project.name}`);
  // Build logic here
  return { status: 'OK' };
}

if (import.meta.main) {
  runJobCommandCli({
    model: BuildOptions,
    extension: '@my/extension',
    job: 'build',
    run: runBuild,
  });
}

Enable your extension

Install your extension at the workspace root:

bun add <your-extension>

The extension will be auto-discovered and available immediately. You can also set workspace defaults in .putnamirc.json under options, or project-level defaults in a .putnamirc.json file under commands.

Next steps

  • Previous: SDK
  • Next: TypeScript

On this page

  • Extensions
  • Install extensions
  • Extension caching
  • Disabling extensions
  • Disabling specific jobs
  • Extension dependencies
  • Job priority
  • Lifecycle hooks
  • Module hooks (legacy)
  • Command hooks
  • Official Extensions
  • @putnami/typescript
  • @putnami/go
  • @putnami/python
  • @putnami/git
  • Any-Language Extensions
  • Structure
  • Manifest with embedded flags
  • Template variables
  • Registering extensions via local paths
  • JSONL event protocol
  • Invocation
  • Event types
  • Example JSONL output
  • Exit codes
  • Example: Shell script extension
  • Example: Python extension
  • Creating Custom Extensions
  • Minimal putnami.extension.json
  • dependsOn scope syntax
  • Job CLI script
  • Enable your extension
  • Next steps