Frontend Contributions
A frontend contribution describes how an integration appears and behaves in the Playrunner web application. It does not execute the integration's workflow action.
Declare and export the surface
Declare the frontend entrypoint in the package manifest and expose the same
subpath through exports:
{
"playrunner": {
"integration": {
"id": "example",
"frontend": "."
}
},
"exports": {
".": {
"types": "./src/frontend/index.tsx",
"import": "./src/frontend/index.tsx",
"require": "./src/frontend/index.tsx",
"default": "./src/frontend/index.tsx"
}
}
}
The entrypoint must default-export an object satisfying the Integration
contract from @playrunner/integration-sdk:
import type { Integration } from '@playrunner/integration-sdk';
import { ExampleConfigPanel } from './ExampleConfigPanel';
import { ExampleSettingsModal } from './ExampleSettingsModal';
import { exampleIconUrl } from './icon';
export const exampleIntegration: Integration = {
id: 'example',
name: 'Example',
category: 'Developer Tools',
description: 'Run an Example action',
icon: exampleIconUrl,
nodeType: 'action',
SettingsModal: ExampleSettingsModal,
ConfigPanel: ExampleConfigPanel,
};
export default exampleIntegration;
The exported id must equal playrunner.integration.id. The contract also
supports visibility and availability flags, authentication metadata, ordering,
package-owned icons, refresh behavior, and optional settings and config
components.
Keep brand assets in the package and resolve them from package source, as the
existing packages do with new URL('../../assets/example.svg', import.meta.url).
Choose the icon pattern by brand behavior
Use the provider's visual behavior to choose between the two supported icon patterns:
- Fixed-color and multicolor logos, such as Jira, Slack, and Playwright, use a
package-owned SVG URL as the integration
icon. Render these logos as normal images so their brand colors remain unchanged. - Monochrome logos that must follow the active theme, such as GitHub and OpenAI,
use a package-owned React SVG component with
fill="currentColor". Set that component as the integrationiconand reuse it in settings and connection UI.
For a theme-adaptive icon, keep the raw asset URL as a separate export when
consumers need it. Do not pass the URL as the integration icon and do not apply
the SVG through CSS mask-image; both approaches can render the SVG as an
opaque rectangle instead of the intended mark.
export function ExampleIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
className={className}
>
<path d="..." />
</svg>
);
}
Then register and reuse the component:
import { ExampleIcon } from './ExampleIcon';
export const exampleIntegration: Integration = {
id: 'example',
name: 'Example',
category: 'Developer Tools',
description: 'Run an Example action',
icon: ExampleIcon,
nodeType: 'action',
SettingsModal: ExampleSettingsModal,
ConfigPanel: ExampleConfigPanel,
};
<IntegrationSettingsModal
icon={<ExampleIcon className="h-5 w-5 text-[var(--foreground)]" />}
{...settingsModalProps}
/>;
Use the SDK host boundary
Package UI accesses Playrunner-owned services through
@playrunner/integration-sdk. IntegrationSdkProvider supplies the host, and
package components call useIntegrationHost() to access authentication,
integration persistence, cloud credentials, environments, secrets, and shared
UI primitives.
Do not import application internals from apps/frontend/src. Keeping the SDK
as the boundary lets the package own its UI without taking a dependency on the
host app's private modules.
For connection forms, use the SDK's connection input and autofill guard helpers so credential fields follow the shared browser-autofill behavior. Keep provider-specific setup, settings, and node configuration components in the provider package.
How the host composes the frontend
Before frontend development, typechecking, or building, the frontend's package script runs the shared composition generator. It scans the frontend app's installed direct production dependencies and writes static imports to:
apps/frontend/src/integrations/generated-package-contributions.ts
apps/frontend/src/integrations/registry.ts validates that every default
export is an object and that its ID matches the package manifest. It then adds
those contributions to the product registry. Duplicate IDs are rejected.
Local file: dependencies resolve through the frontend app's installed
node_modules; Vite preserves symlinks for those package imports. There is no
provider-specific Vite or TypeScript alias to add.
Every locally edited package that contributes to the rendered feature must use
a file: dependency. For example, local GitHub repository selection requires
both entries below because the modal belongs to GitHub while the selector
belongs to Playwright:
{
"dependencies": {
"@playrunner/github": "file:../../packages/github",
"@playrunner/playwright": "file:../../packages/playwright"
}
}
After installing, verify the selected modules before debugging runtime state:
npm --prefix apps/frontend ls @playrunner/github @playrunner/playwright --depth=0
Both entries should point into ../../packages. If either resolves to an npm
tarball, the frontend can show old package behavior even though the workspace
source and another consuming app are current.
Adding a frontend package or changing its metadata still requires updating the frontend artifact's selected dependency and rebuilding the frontend. Connecting credentials or editing a workflow node after deployment only configures the already-bundled contribution.