Custom Interaction Type
EdgeInteract is fully extensible. You can define your own interaction type with a custom payload schema, a custom response schema, a custom React renderer, and a custom server-side response validator. Custom types participate in the same audit, metrics, and observability pipeline as built-in types.
When to Define a Custom Type
- None of the built-in types (approval, confirmation, form, picker, notification, rating) fit your UX requirement
- You need a highly domain-specific interaction — e.g., a contract line-item comparison, a side-by-side diff review, or a structured interview form
- You want to build reusable interaction components across multiple BizFirstGO applications
Implementation Steps
-
Choose a namespaced type key
Use a namespaced key like
"acme/peer-review"to avoid collision with built-in types. Built-in type keys are reserved (approval, confirmation, form, picker, notification, rating). -
Define the payload TypeScript interface
Define and export the payload shape that the sender will pass and the renderer will receive.
-
Define the response TypeScript interface
Define the exact shape of
InteractionResponse.dataand the validoutcomestring values. -
Build the React renderer component
Implement the UI component that receives the interaction and calls
respond()when the user submits. -
Register the renderer with InteractionContainer
Call
registerInteractionRenderer(typeKey, Component)so EdgeInteract knows how to render your type. -
Implement and register a server-side response validator
Implement
IInteractionResponseValidatorso the server validates responses for your type.
Step 2 — Payload and Response Interfaces
// types/peer-review.ts
export interface PeerReviewPayload {
revieweeName: string;
reviewPeriod: string;
criteria: {
key: string;
label: string;
}[];
}
export interface PeerReviewResponseData {
scores: Record<string, 1 | 2 | 3 | 4 | 5>; // criteriaKey → score
strengths: string;
improvements: string;
}
export type PeerReviewOutcome = 'submitted' | 'declined';
Step 4 — React Renderer Component
// components/PeerReviewComponent.tsx
import type { InteractionRendererProps } from 'edge-interact-react';
import type { PeerReviewPayload, PeerReviewResponseData } from '../types/peer-review';
export function PeerReviewComponent({
interaction,
respond
}: InteractionRendererProps<PeerReviewPayload>) {
const { criteria, revieweeName, reviewPeriod } = interaction.payload;
const [scores, setScores] = useState<Record<string, number>>({});
const [strengths, setStrengths] = useState('');
const [improvements, setImprovements] = useState('');
const handleSubmit = () => {
const data: PeerReviewResponseData = { scores, strengths, improvements };
respond('submitted', data);
};
return (
<div className="peer-review">
<h3>Peer Review: {revieweeName} ({reviewPeriod})</h3>
{criteria.map(c => (
<div key={c.key}>
<label>{c.label}</label>
<StarRating
value={scores[c.key]}
onChange={v => setScores(s => ({ ...s, [c.key]: v }))}
/>
</div>
))}
<textarea
placeholder="Key strengths..."
value={strengths}
onChange={e => setStrengths(e.target.value)}
/>
<textarea
placeholder="Areas for improvement..."
value={improvements}
onChange={e => setImprovements(e.target.value)}
/>
<button onClick={handleSubmit}>Submit Review</button>
<button onClick={() => respond('declined', {})}>I cannot complete this review</button>
</div>
);
}
Step 5 — Register the Renderer
// In your app's InteractionContainer setup
import { interactionContainer } from 'edge-interact-react';
import { PeerReviewComponent } from './components/PeerReviewComponent';
interactionContainer.registerInteractionRenderer(
'acme/peer-review',
PeerReviewComponent
);
Step 6 — Server-Side Response Validator
// Server: PeerReviewResponseValidator.cs
public class PeerReviewResponseValidator : IInteractionResponseValidator
{
public string TypeKey => "acme/peer-review";
public ValidationResult Validate(InteractionResponse response)
{
if (response.Outcome is not ("submitted" or "declined"))
return ValidationResult.Fail("outcome must be 'submitted' or 'declined'");
if (response.Outcome == "submitted")
{
if (!response.Data.ContainsKey("scores"))
return ValidationResult.Fail("scores is required when outcome is submitted");
if (string.IsNullOrWhiteSpace(response.Data.GetString("strengths")))
return ValidationResult.Fail("strengths is required when outcome is submitted");
}
return ValidationResult.Ok();
}
}
// Registration in Program.cs
builder.Services.AddInteractionResponseValidator<PeerReviewResponseValidator>();
Sending a Custom Type Interaction
const response = await sendInteraction({
type: 'acme/peer-review',
targetUserId: 'usr_reviewer_bob',
title: `Peer Review: Alice Johnson (Q2 2026)`,
payload: {
revieweeName: 'Alice Johnson',
reviewPeriod: 'Q2 2026',
criteria: [
{ key: 'collaboration', label: 'Collaboration' },
{ key: 'delivery', label: 'Delivery & Reliability' },
{ key: 'communication', label: 'Communication' },
{ key: 'initiative', label: 'Initiative' }
]
} satisfies PeerReviewPayload,
timeoutMs: 604_800_000
});
if (response.outcome === 'submitted') {
await hrService.saveReview(reviewId, response.data as PeerReviewResponseData);
}
Custom Type Checklist
| Item | Required? | Notes |
|---|---|---|
| Namespaced type key | Yes | e.g. "acme/peer-review" |
| Payload TypeScript interface | Recommended | Enforced at call site with satisfies |
| Response data TypeScript interface | Recommended | Used when casting response.data |
| React renderer component | Yes | Receives interaction and respond |
| Renderer registration | Yes | interactionContainer.registerInteractionRenderer() |
| Server-side response validator | Strongly recommended | IInteractionResponseValidator |
| Validator registration | Yes (if validator defined) | AddInteractionResponseValidator<T>() |
AuditInteractionHook and MetricsInteractionHook — no additional registration needed. Metrics are tagged with your custom type key, so you can filter dashboards and alerts by type.