resonate-lovable-usage-prompt-typescript — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited resonate-lovable-usage-prompt-typescript (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
SDK version: This skill reflects @resonatehq/sdk v0.10.0 (current on npm).This skill provides specialized guidance for building Resonate durable execution applications within Lovable.dev, an AI-assisted full-stack development environment. Lovable enables rapid prototyping and deployment of React + Node.js applications with built-in hosting.
Critical insight: For workflow state, Resonate is your single source of truth. You don't need a separate database to track workflow status, pending approvals, or execution state.
Traditional approach (unnecessary complexity):
Create workflow → Store in DB → Poll DB for status → Sync with Resonate
Resonate approach (simple):
Create workflow → Query Resonate directly → DoneWhy this matters:
When you DO need a database:
When you DON'T need a database:
Lovable does NOT provide:
Implication: You cannot run a persistent Resonate worker inside Lovable's backend.
Lovable backend acts as HTTP client to external Resonate:
Lovable Frontend (React)
↓
Lovable Backend (Express) [HTTP Client Only]
↓
External Resonate Server (Cloud Run, Fly.io, Railway)
↓
Resonate Workers (separate deployment)Lovable backend code:
// server/routes/workflows.ts
import { Resonate } from "@resonatehq/sdk";
import express from "express";
const router = express.Router();
// Connect to external Resonate server
const resonate = new Resonate({
url: process.env.RESONATE_URL, // e.g., https://resonate.example.com
token: process.env.RESONATE_TOKEN, // JWT token for auth (if server requires it)
group: "lovable-client"
});
// Start a workflow (non-blocking)
router.post("/api/workflows/start", async (req, res) => {
const { workflowType, input } = req.body;
const workflowId = `workflow-${Date.now()}`;
await resonate.beginRpc(
workflowId,
workflowType,
input,
resonate.options({
target: "poll://any@workers" // Routes to external workers
})
);
res.json({
workflowId,
status: "started",
pollUrl: `/api/workflows/${workflowId}/status`
});
});
// Check workflow status
router.get("/api/workflows/:id/status", async (req, res) => {
try {
const handle = await resonate.get(req.params.id);
const result = await handle.result();
res.json({
workflowId: req.params.id,
status: "completed",
result
});
} catch (error) {
res.json({
workflowId: req.params.id,
status: "pending"
});
}
});
export default router;External workers (deployed separately):
// workers/index.ts (deployed to Cloud Run/Fly.io)
import { Resonate, type Context } from "@resonatehq/sdk";
const resonate = new Resonate({
url: process.env.RESONATE_URL,
token: process.env.RESONATE_TOKEN, // JWT token for auth
group: "workers"
});
function* processOrder(ctx: Context, input: any) {
// Durable workflow logic
const validated = yield* ctx.run(validateOrder, input);
const payment = yield* ctx.run(processPayment, validated);
const shipment = yield* ctx.run(createShipment, payment);
return { status: "success", shipment };
}
resonate.register("processOrder", processOrder);
// Keep worker alive
process.on("SIGTERM", () => process.exit(0));If using Lovable with Supabase Edge Functions, see the resonate-supabase-deployments-typescript skill for complete Deno-specific patterns including:
@resonatehq/supabase shim usagestart/ and probe/ endpoint patternsLimitation: Supabase Edge Functions have 30-second timeout, so workflows must complete quickly or use async patterns.
This is the most important decision when building with Lovable + Resonate.
| Action | SDK Method | Example |
|---|---|---|
| Start a workflow | resonate.run(), resonate.rpc(), beginRun(), beginRpc() | Starting an order processing workflow |
| Execute durable code | Generator functions with ctx.run(), ctx.sleep() | The workflow logic itself |
| Register workflow handlers | resonate.register() | Setting up workers |
Key insight: The SDK is for executing workflows. You need a process that can run the workflow code.
| Action | HTTP Method | Example |
|---|---|---|
| List promises by prefix | GET /promises?id=prefix-* | Showing all pending approvals in UI |
| Get promise state | GET /promises/{id} | Checking if a workflow completed |
| Resolve a HITL promise | PATCH /promises/{id} | User clicking "Approve" button |
| Create a standalone promise | POST /promises | External system creating a promise to be resolved later |
Key insight: The HTTP API is for managing promise state without running workflow code.
Do you need to RUN workflow code (generators, ctx.run, ctx.sleep)?
│
├─ YES → Use SDK: resonate.run(), resonate.rpc(), etc.
│ (Requires a worker process that can execute the code)
│
└─ NO → Are you querying or resolving existing promises?
│
├─ YES → Use HTTP API: GET/PATCH /promises
│ (Can be done from any HTTP client)
│
└─ NO → You probably need the SDKSince Lovable cannot run persistent workers, your Lovable backend should:
resonate.beginRpc() with target: "poll://any@workers"GET /promises?id=...PATCH /promises/{id} or resonate.promises.settle(id, "resolved", { data })The actual workflow EXECUTION happens on your external workers (Cloud Run, Fly.io, etc.), not in Lovable.
Frontend (React):
// src/components/WorkflowRunner.tsx
import { useState } from "react";
import { useToast } from "@/components/ui/use-toast";
export function WorkflowRunner() {
const [workflowId, setWorkflowId] = useState<string | null>(null);
const [status, setStatus] = useState<"idle" | "running" | "completed">("idle");
const { toast } = useToast();
const startWorkflow = async () => {
setStatus("running");
const response = await fetch("/api/workflows/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
workflowType: "processOrder",
input: { orderId: "123", items: [...] }
})
});
const { workflowId } = await response.json();
setWorkflowId(workflowId);
// Poll for completion
const pollInterval = setInterval(async () => {
const statusRes = await fetch(`/api/workflows/${workflowId}/status`);
const data = await statusRes.json();
if (data.status === "completed") {
clearInterval(pollInterval);
setStatus("completed");
toast({ title: "Workflow completed!", description: JSON.stringify(data.result) });
}
}, 2000);
};
return (
<div>
<button onClick={startWorkflow} disabled={status === "running"}>
Start Workflow
</button>
{status === "running" && <p>Workflow running... (ID: {workflowId})</p>}
{status === "completed" && <p>Workflow completed!</p>}
</div>
);
}Track workflow progress in Lovable's built-in PostgreSQL:
// server/routes/workflows.ts
router.post("/api/workflows/start", async (req, res) => {
const { workflowType, input } = req.body;
const workflowId = `workflow-${Date.now()}`;
// Store in database
await supabase.from("workflows").insert({
id: workflowId,
type: workflowType,
input,
status: "pending",
created_at: new Date().toISOString()
});
// Start Resonate workflow
await resonate.beginRpc(workflowId, workflowType, input, resonate.options({
target: "poll://any@workers"
}));
res.json({ workflowId });
});
// Webhook for completion (called by external worker)
router.post("/api/workflows/:id/complete", async (req, res) => {
const { id } = req.params;
const { result } = req.body;
await supabase.from("workflows").update({
status: "completed",
result,
completed_at: new Date().toISOString()
}).eq("id", id);
res.json({ success: true });
});Workflow creates approval request, Lovable UI resolves it:
// External worker
function* approvalWorkflow(ctx: Context, orderId: string) {
const promise = yield* ctx.promise({
id: `approval-${orderId}`,
timeout: 24 * 60 * 60 * 1000
});
// Notify Lovable backend
yield* ctx.run(async () => {
await fetch(`${lovableBackendUrl}/api/approvals/create`, {
method: "POST",
body: JSON.stringify({
orderId,
promiseId: promise.id
})
});
});
const decision = yield* promise;
return decision;
}
// Lovable backend
router.post("/api/approvals/resolve/:promiseId", async (req, res) => {
const { promiseId } = req.params;
const { approved } = req.body;
const data = Buffer.from(JSON.stringify({ approved })).toString('base64');
await resonate.promises.settle(promiseId, "resolved", { data });
res.json({ success: true });
});
// Lovable frontend
function ApprovalButton({ promiseId }: { promiseId: string }) {
const handleApprove = async () => {
await fetch(`/api/approvals/resolve/${promiseId}`, {
method: "POST",
body: JSON.stringify({ approved: true })
});
};
return <button onClick={handleApprove}>Approve</button>;
}Options:
Example (Cloud Run):
# Deploy Resonate server
docker pull resonatehq/resonate:latest
gcloud run deploy resonate-server \
--image resonatehq/resonate:latest \
--platform managed \
--region us-central1 \
--allow-unauthenticatedSame platforms, separate service:
# Build and deploy worker
docker build -t workers .
gcloud run deploy resonate-workers \
--image gcr.io/PROJECT/workers \
--platform managed \
--set-env-vars RESONATE_URL=https://resonate-server-xxx.run.appSet environment variable in Lovable:
RESONATE_URL=https://resonate-server-xxx.run.appWhen using Lovable with an external Resonate server, you'll make direct HTTP calls. Here's the complete API reference.
const RESONATE_URL = "https://resonate-server.example.com"; // Your Resonate server
const RESONATE_TOKEN = process.env.RESONATE_TOKEN; // Optional auth token
const headers = {
"Content-Type": "application/json",
...(RESONATE_TOKEN && { "Authorization": `Bearer ${RESONATE_TOKEN}` })
};Resonate promises have these states:
| State | Description |
|---|---|
PENDING | Promise created, waiting for resolution |
RESOLVED | Promise completed successfully |
REJECTED | Promise failed/rejected |
REJECTED_TIMEDOUT | Promise exceeded timeout |
REJECTED_CANCELED | Promise was canceled |
Endpoint: POST /promises
Request:
{
"id": "approval-request-abc123",
"timeout": 86400000,
"param": {
"headers": {},
"data": "eyJvcmRlcklkIjoiMTIzIn0="
},
"tags": {
"type": "approval",
"created_by": "[email protected]"
}
}Notes:
id: Use a prefix convention like approval-request- for easy queryingtimeout: Epoch milliseconds from now (NOT ISO string). Example: 86400000 = 24 hoursparam.data: Base64-encoded JSON for initial data (optional)tags: Key-value metadata for filtering (optional)Response:
{
"id": "approval-request-abc123",
"state": "PENDING",
"timeout": 1706486400000,
"param": { "headers": {}, "data": "eyJvcmRlcklkIjoiMTIzIn0=" },
"value": { "headers": {}, "data": null },
"tags": { "type": "approval" },
"createdOn": 1706400000000,
"completedOn": null
}TypeScript Example:
async function createApprovalRequest(requestId: string, data: any) {
const response = await fetch(`${RESONATE_URL}/promises`, {
method: "POST",
headers,
body: JSON.stringify({
id: `approval-request-${requestId}`,
timeout: Date.now() + (24 * 60 * 60 * 1000), // 24 hours from now
param: {
headers: {},
data: btoa(JSON.stringify(data)) // Base64 encode
}
})
});
return response.json();
}Endpoint: PATCH /promises/{id}
Request (approve):
{
"state": "RESOLVED",
"value": {
"data": "eyJhcHByb3ZlZCI6dHJ1ZX0="
}
}Request (reject):
{
"state": "REJECTED",
"value": {
"data": "eyJhcHByb3ZlZCI6ZmFsc2UsInJlYXNvbiI6IkJ1ZGdldCBleGNlZWRlZCJ9"
}
}Notes:
value.data: MUST be Base64-encoded JSONstate to RESOLVED for approval, REJECTED for rejectionTypeScript Example:
async function resolveApproval(promiseId: string, approved: boolean, reason?: string) {
const decision = { approved, reason, resolvedAt: Date.now() };
const encodedData = btoa(JSON.stringify(decision)); // Base64 encode
const response = await fetch(`${RESONATE_URL}/promises/${promiseId}`, {
method: "PATCH",
headers,
body: JSON.stringify({
state: approved ? "RESOLVED" : "REJECTED",
value: { data: encodedData }
})
});
return response.json();
}Endpoint: GET /promises/{id}
Response:
{
"id": "approval-request-abc123",
"state": "PENDING",
"timeout": 1706486400000,
"param": { "headers": {}, "data": "eyJvcmRlcklkIjoiMTIzIn0=" },
"value": { "headers": {}, "data": null },
"tags": { "type": "approval" },
"createdOn": 1706400000000,
"completedOn": null
}TypeScript Example:
async function getApprovalRequest(promiseId: string) {
const response = await fetch(`${RESONATE_URL}/promises/${promiseId}`, {
method: "GET",
headers
});
const promise = await response.json();
// Decode the data if present
if (promise.param?.data) {
promise.decodedParam = JSON.parse(atob(promise.param.data));
}
if (promise.value?.data) {
promise.decodedValue = JSON.parse(atob(promise.value.data));
}
return promise;
}Endpoint: GET /promises?id={prefix}*&limit={n}&state={state}
Query Parameters:
id: Prefix pattern with wildcard, e.g., approval-request-*limit: Max results (default 10, max usually 100)state: Filter by state: pending, resolved, rejectedExample Queries:
GET /promises?id=approval-request-*&limit=50
GET /promises?id=approval-request-*&state=pending&limit=20
GET /promises?id=countdown-*&state=pendingResponse:
{
"promises": [
{
"id": "approval-request-abc123",
"state": "PENDING",
"timeout": 1706486400000,
...
},
{
"id": "approval-request-def456",
"state": "RESOLVED",
...
}
],
"cursor": "next-page-token"
}TypeScript Example:
async function listPendingApprovals() {
const response = await fetch(
`${RESONATE_URL}/promises?id=approval-request-*&state=pending&limit=50`,
{ method: "GET", headers }
);
const { promises } = await response.json();
// Decode all param data
return promises.map(p => ({
...p,
decodedParam: p.param?.data ? JSON.parse(atob(p.param.data)) : null
}));
}// Create approval - Resonate is the source of truth
app.post("/api/approvals", async (req, res) => {
const requestId = crypto.randomUUID();
const promise = await createApprovalRequest(requestId, req.body);
res.json({ requestId, promiseId: promise.id });
});
// List pending approvals - Query Resonate directly
app.get("/api/approvals", async (req, res) => {
const approvals = await listPendingApprovals();
res.json(approvals);
});
// Get single approval
app.get("/api/approvals/:id", async (req, res) => {
const approval = await getApprovalRequest(`approval-request-${req.params.id}`);
res.json(approval);
});
// Resolve approval
app.post("/api/approvals/:id/resolve", async (req, res) => {
const { approved, reason } = req.body;
await resolveApproval(`approval-request-${req.params.id}`, approved, reason);
res.json({ success: true });
});IMPORTANT: Timeouts are absolute epoch milliseconds, not durations.
// ❌ WRONG - This is a duration, not a timestamp
const timeout = 24 * 60 * 60 * 1000;
// ✅ CORRECT - Absolute timestamp (now + duration)
const timeout = Date.now() + (24 * 60 * 60 * 1000);
// Common patterns:
const in1Hour = Date.now() + (60 * 60 * 1000);
const in24Hours = Date.now() + (24 * 60 * 60 * 1000);
const in7Days = Date.now() + (7 * 24 * 60 * 60 * 1000);Create a Resonate client module in server/lib/resonate.ts that:
1. Connects to process.env.RESONATE_URL
2. Uses group "lovable-client"
3. Exports a configured Resonate instance
4. Includes TypeScript types for workflow inputs/outputsCreate Express routes in server/routes/workflows.ts that:
1. POST /api/workflows/start - starts a workflow via resonate.beginRpc
2. GET /api/workflows/:id/status - checks workflow status
3. POST /api/workflows/:id/cancel - cancels a workflow
4. Include error handling and TypeScript typesCreate a React component WorkflowDashboard that:
1. Lists all workflows from Supabase "workflows" table
2. Shows workflow status (pending/running/completed/failed)
3. Allows starting new workflows
4. Polls for status updates every 3 seconds
5. Uses shadcn/ui components (button, card, badge)Use case: Order processing with approval
// worker/index.ts
import { Resonate, type Context } from "@resonatehq/sdk";
const resonate = new Resonate({
url: process.env.RESONATE_URL,
token: process.env.RESONATE_TOKEN, // JWT token for auth
group: "workers"
});
function* processOrder(ctx: Context, order: any) {
// Validate
const valid = yield* ctx.run(validateOrder, order);
// Create approval promise
const approval = yield* ctx.promise({
id: `approval-${order.id}`,
timeout: 24 * 60 * 60 * 1000
});
// Notify Lovable
yield* ctx.run(notifyLovable, order.id, approval.id);
// Wait for human approval
const decision = yield* approval;
if (!decision.approved) {
return { status: "rejected" };
}
// Process payment
const payment = yield* ctx.run(chargePayment, order);
return { status: "approved", payment };
}
resonate.register("processOrder", processOrder);// server/routes/orders.ts
import { resonate, supabase } from "../lib";
router.post("/api/orders", async (req, res) => {
const order = req.body;
const orderId = `order-${Date.now()}`;
// Store in database
await supabase.from("orders").insert({
id: orderId,
data: order,
status: "pending"
});
// Start workflow
await resonate.beginRpc(
orderId,
"processOrder",
order,
resonate.options({ target: "poll://any@workers" })
);
res.json({ orderId });
});
router.post("/api/approvals/:promiseId", async (req, res) => {
const { promiseId } = req.params;
const { approved } = req.body;
const data = Buffer.from(JSON.stringify({ approved })).toString('base64');
await resonate.promises.settle(promiseId, "resolved", { data });
res.json({ success: true });
});// src/pages/Orders.tsx
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
export function Orders() {
const [orders, setOrders] = useState([]);
const createOrder = async () => {
const response = await fetch("/api/orders", {
method: "POST",
body: JSON.stringify({
items: ["item1", "item2"],
total: 100
})
});
const { orderId } = await response.json();
// Refresh list
};
return (
<div>
<Button onClick={createOrder}>Create Order</Button>
<OrderList orders={orders} />
</div>
);
}When building with Lovable + Resonate, tell users:
Lovable + Resonate enables rapid development of durable execution applications by:
Key principle: Lovable handles the user interface, Resonate handles the durable execution.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.