This is the dev preview website. Check out the document at lynxjs.org

System prompts

Generate and customize the instructions that teach an LLM to emit valid OpenUI Lang for the component Library your ReactLynx client renders.

Use the CLI when the built-in Library and a static prompt file are enough. Use the programmatic API when tools, feature flags, custom components, or deployment policy must be assembled in backend code.

The contract to preserve

An OpenUI system prompt describes:

  • the one-assignment-per-line language syntax;
  • the required root entry point and top-down streaming order;
  • component signatures derived from ordered Zod schemas;
  • reactive $variables and built-in functions;
  • Query/Mutation and Action rules;
  • tool names and input/output schemas, when supplied;
  • examples and hard constraints for parseable output.

The prompt-side Library and client renderer Library must agree on every component name and positional prop order. If they drift, the model can produce valid text for one side that the other side rejects.

1. CLI

Generate the default built-in OpenUI prompt to a file:

npx @lynx-js/genui openui generate prompt \
  --out dist/openui-system-prompt.txt

Print it to stdout:

npx @lynx-js/genui openui generate prompt

Append deployment-specific rules:

npx @lynx-js/genui openui generate prompt \
  --appendix "Prefer compact mobile layouts for booking flows." \
  --out dist/openui-system-prompt.txt

The CLI uses the headless mirror of the built-in ReactLynx Library. Its prompt generation command accepts:

OptionPurpose
--out <file>Write to a file instead of stdout. Parent directories are created.
--appendix <text>Append application-specific instructions.
--versionPrint the package version.
--helpPrint command help.

The CLI does not load a custom component directory or tool manifest. Use the programmatic API when the Agent contract contains custom components or rich tool schemas.

2. Programmatic usage

Build the default prompt in server-side TypeScript:

import { buildOpenUiSystemPrompt } from '@lynx-js/genui/openui/prompt';

const systemPrompt = buildOpenUiSystemPrompt();

Add application policy and tool descriptions:

import { buildOpenUiSystemPrompt } from '@lynx-js/genui/openui/prompt';

const systemPrompt = buildOpenUiSystemPrompt({
  appendix: [
    'Prefer one-column mobile layouts.',
    'Never invent tool names or arguments.',
  ].join('\n'),
  promptOptions: {
    tools: [
      {
        name: 'list_orders',
        description: 'List orders filtered by status.',
        inputSchema: {
          type: 'object',
          properties: { status: { type: 'string' } },
          required: ['status'],
        },
        outputSchema: {
          type: 'object',
          properties: {
            rows: { type: 'array', items: { type: 'object' } },
          },
          required: ['rows'],
        },
        annotations: { readOnlyHint: true },
      },
    ],
  },
});

The client must expose a matching tool implementation:

<OpenUiRenderer
  response={response}
  library={library}
  toolProvider={{
    list_orders: async ({ status }) => {
      return await api.listOrders({ status: String(status) });
    },
  }}
/>;

Tool schemas teach the model what it may call; toolProvider is the code that actually executes those calls. Do not put secrets or provider credentials in the prompt or browser-side tool arguments.

Prompt options

buildOpenUiSystemPrompt accepts Library options, prompt feature flags, and an optional appendix:

OptionPurpose
rootOverride the required root component name. The component must exist in the prompt Library and client Library.
componentsAppend headless component definitions; later duplicate names replace defaults.
componentGroupsAppend prompt grouping metadata.
promptOptions.preambleReplace or extend the product framing before language rules.
promptOptions.additionalRulesAdd rules after the built-in mobile/OpenUI constraints.
promptOptions.examplesReplace the default static examples.
promptOptions.toolExamplesProvide examples used when tools are present.
promptOptions.toolsDescribe Query/Mutation tools by name or rich schema.
promptOptions.toolCallsEnable or disable Query, Mutation, @Run, and tool workflow instructions.
promptOptions.bindingsEnable or disable $variables, @Set, @Reset, and reactive filter instructions.
promptOptions.editModeTeach the model to return only changed statements for incremental editing.
promptOptions.inlineModeAllow prose with optional fenced OpenUI instead of raw OpenUI-only output.
appendixAppend final deployment-specific instructions verbatim.

The Lynx OpenUI prompt builder enables bindings and toolCalls by default. Disable them explicitly for a static-only product surface:

const systemPrompt = buildOpenUiSystemPrompt({
  promptOptions: {
    bindings: false,
    toolCalls: false,
  },
});

inlineMode is disabled by default. In the default mode, the model should return raw OpenUI Lang only—no explanation and no Markdown code fence—so the full response can go directly into <OpenUiRenderer response={...}>.

Custom component prompts

Keep shared Zod schemas in a framework-neutral module. The client wraps the schema with the ReactLynx defineComponent; the server wraps the same schema with the headless @openuidev/lang-core definition.

// shared/bannerSchema.ts
import { z } from 'zod/v4';

export const bannerProps = z.object({
  title: z.string(),
  tone: z.enum(['info', 'success', 'warning']).optional(),
});
// client/Banner.tsx
import { defineComponent } from '@lynx-js/genui/openui';
import { bannerProps } from '../shared/bannerSchema.js';

export const Banner = defineComponent({
  name: 'Banner',
  description: 'Compact status banner with a title and tone.',
  props: bannerProps,
  component: ({ props }) => (
    <view className={`Banner Banner-${props.tone ?? 'info'}`}>
      <text className='BannerTitle'>{props.title}</text>
    </view>
  ),
});
// server/openuiPrompt.ts
import { defineComponent } from '@openuidev/lang-core';
import { buildOpenUiSystemPrompt } from '@lynx-js/genui/openui/prompt';
import { bannerProps } from '../shared/bannerSchema.js';

const PromptBanner = defineComponent({
  name: 'Banner',
  description: 'Compact status banner with a title and tone.',
  props: bannerProps,
  component: () => null,
});

export const systemPrompt = buildOpenUiSystemPrompt({
  components: [PromptBanner],
  componentGroups: [
    { name: 'Product', components: ['Banner'] },
  ],
});

Install @openuidev/lang-core and zod as direct dependencies of the server workspace when it owns custom headless definitions. Keeping the prompt component renderer as () => null prevents server routes from importing ReactLynx, Lynx UI components, or component CSS.

Other prompt exports

@lynx-js/genui/openui/prompt also exports:

ExportPurpose
createOpenUiPromptLibrary(options)Build the headless built-in Library for inspection or lower-level prompt composition.
OPENUI_SYSTEM_PROMPTPrebuilt default prompt created with no options.
openUiPromptActionPropSchemaShared prompt schema for v0.5 action plans and legacy action objects.

Prefer buildOpenUiSystemPrompt() in application code so customization remains explicit and testable.

Backend integration pattern

Send the generated prompt as the model's system instruction, preserve the raw text stream, and return that text to the client. The server does not need to parse the UI simply to forward it, but it should enforce its own model-output and tool policies.

const systemPrompt = buildOpenUiSystemPrompt({
  promptOptions: { tools: toolSpecs },
  appendix: productPolicy,
});

const stream = await model.generate({
  system: systemPrompt,
  messages,
});

return streamOpenUiText(stream);

On the client, append every received delta to one string. Pass that accumulated value to response and keep isStreaming={true} until the server signals completion. Use an AbortController to cancel an older generation before a new turn starts.

When onError reports stable parser/runtime errors, an application may send a compact description back to the Agent for correction. Do not automatically retry forever; bound correction attempts and retain the original user request.

Choosing an approach

Use the CLI when:

  • the built-in component contract is sufficient;
  • a checked-in or generated static prompt file is convenient;
  • deployment policy fits in an appendix.

Use programmatic generation when:

  • tools have request- or deployment-specific schemas;
  • feature flags differ between products;
  • the Library contains custom or overridden components;
  • examples, groups, or policy are assembled from code.

For the renderer-side contract, continue with Libraries, built-ins, and custom components.