Cordis plugin developer tutorial

Build your first Cordis plugin

Create a minimal TypeScript plugin, load it into the DeepSeek Harness Web UI, understand automatic cleanup, and learn when to declare services—all from one runnable example.

LevelBeginnerEstimated time15–20 minutesYou will buildA local hello plugin

Source discipline

This independent tutorial follows the current official developer-preview documentation. Verify commands and APIs upstream before using them in production.

Official tutorialSource repository

00 · Before you start

Before you start

The official tutorial assumes you can already run DeepSeek Harness from a source checkout. Work from the repository root so the commands and patch path resolve as shown.

  • A local checkout of deepseek-ai/deepseek-harness
  • Project dependencies installed with the repository package manager
  • A terminal at the repository root
  • Node.js and pnpm versions compatible with the checked-out revision

DeepSeek Harness and its plugin contracts are in developer preview. Pin the revision used for development.

Step 1

Create a local plugin project

Create a temporary project inside the Harness repository. Keeping this first experiment local makes the loading path explicit and easy to remove.

shell
mkdir -p scratch-plugin/src

Step 2

Write the plugin module

A Harness plugin is a TypeScript module that exports an apply function. The framework calls apply with a Cordis Context; use that context to register capabilities.

Create scratch-plugin/src/my-plugin.ts with the following minimal implementation. The console message gives us an observable verification signal.

typescript
import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  // Required dependencies are ready before apply runs.
  console.log('[hello-plugin] plugin loaded!')
}

Step 3

Register it in cordis.yml

Run pwd at the repository root, then create scratch-plugin/cordis.yml as a Web configuration overlay. Replace the example repository path with the absolute path printed by pwd.

yaml
- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
The plugin path must be absolute. A patch contributes configuration but does not change the profile directory used by the module loader.

Step 4

Start the Web UI with the overlay

Start the Web UI from the repository root with your patch file. Then open http://127.0.0.1:3080.

shell
pnpm dsh web --patch ./scratch-plugin/cordis.yml
A successful load prints [hello-plugin] plugin loaded! in the terminal during startup.

05 · Cordis

Lifecycle and service dependencies

Cordis scopes registrations to the plugin context. Understand these two mechanisms before your plugin owns timers, connections, tools, or other shared capabilities.

Clean up side effects with ctx.effect()

Context-managed registrations are removed with the plugin. For resources that need explicit disposal—such as timers or network connections—return a cleanup function from ctx.effect().

typescript
import type { Context } from '@deepseek-ai/cordis'

export function apply(ctx: Context) {
  ctx.effect(() => {
    const timer = setInterval(() => {
      console.log('heartbeat')
    }, 5000)

    // Runs automatically when the plugin unloads.
    return () => clearInterval(timer)
  })
}

Declare required services with inject

If a plugin needs tools, llm, or another service, declare it in inject. The framework waits for those services before calling apply.

typescript
import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-tool-plugin'
export const inject = ['tools']

export function apply(ctx: Context) {
  // ctx.tools is ready here.
  ctx.tools.register(/* ... */)
}

06 · API

Choose the smallest plugin form

Cordis accepts function, object, and class forms. Start with a function; move to a service class only when the plugin must provide a service to other plugins.

Function

The default for focused capabilities. It is easy to read, test, and unload.

Object

Useful when name, inject, apply, and related metadata belong together.

Service class

Use when the plugin exposes a named Cordis service and owns service lifecycle.

Object

typescript
export default {
  name: 'my-plugin',
  inject: ['tools'],
  apply(ctx: Context) {
    // Register capabilities here.
  },
}

Service class

typescript
import { Service, type Context } from '@deepseek-ai/cordis'

export default class MyService extends Service {
  static inject = ['tools']

  constructor(ctx: Context) {
    super(ctx, 'myService')
    // Perform synchronous initialization here.
  }
}

07 · Verify

Verification checklist

Do not stop when the process starts. Confirm the load path, lifecycle, and removal behavior.

  • The Web UI opens at 127.0.0.1:3080.
  • The terminal shows the hello-plugin load message once.
  • Changing the absolute path to an invalid file produces an understandable load error.
  • Stopping or unloading the plugin cleans up timers and other effects.
  • Removing the patch restores the original Web profile behavior.

Common problems

Module cannot be resolved+

Confirm the path in cordis.yml is absolute, points to the .ts file, and matches the current checkout.

Plugin does not log+

Run the command from the repository root and confirm --patch points to ./scratch-plugin/cordis.yml.

Service is undefined+

Add the service name to inject and access it only after apply has been called.

Port 3080 is unavailable+

Stop the process already using the port or follow the current upstream Web UI options.

Continue developing

Continue developing

The minimal plugin proves loading and lifecycle. Next, build a real tool, add validated configuration, package the plugin, and test it against a pinned Harness revision.