Developer documentation

Build reliable wrapper-code workflows.

Learn how to authenticate, run wrapper codes, upload files, download artifacts, track usage, and integrate Zywrap into production applications.

API-first workflow guide

Move from wrapper discovery to production API calls.

Wrapper-code runsPOST /v2/runs
FilesUpload and attach inputs
ArtifactsDownload generated outputs
Production controlsKeys, retries, usage ledger
Back to SDKs

Node.js SDK Guide

A complete, runnable example for integrating Zywrap Enterprise catalog metadata (including dynamic schemas) into your Node.js (Express) application.

Option 1: Cloud Proxy (Quick Start)

Don't want to host Enterprise catalog metadata yourself? Use our official NPM package to connect directly to the Zywrap Cloud Proxy in seconds.

bash
npm install zywrap
javascript
// Live V2 API call. Keep zw_live keys server-side.
const response = await fetch('https://api.zywrap.com/v2/runs', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.ZYWRAP_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    wrapperCodes: ['marketing_copywriting_cold_out_ema_b2b'],
    model: 'openai-gpt-4o-mini',
    input: {
      text: 'Product: Zywrap
Audience: SaaS developers
Goal: Write a concise outbound email.'
    },
    output: { format: 'markdown', artifact: true }
  })
});

if (!response.ok) {
  throw new Error(await response.text());
}

const data = await response.json();
console.log(data.run.outputText);

Complete V2 file + artifact workflow

Use this when your app needs to upload a file, attach that file to a wrapper-code run, save the output as an artifact, and download the generated result.

javascript
// Complete V2 workflow: upload a file, run a wrapper, download the artifact.
// Requirements: npm install axios form-data
// Keep zw_live keys on your server, not in browser/mobile clients.

const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const crypto = require('crypto');

const API = 'https://api.zywrap.com';
const API_KEY = process.env.ZYWRAP_API_KEY;

function authHeaders(extra = {}) {
  return {
    Authorization: 'Bearer ' + API_KEY,
    ...extra,
  };
}

async function uploadFile(filePath) {
  const form = new FormData();
  form.append('file', fs.createReadStream(filePath));
  form.append('displayName', 'Customer feedback CSV');

  const response = await axios.post(API + '/v2/files', form, {
    headers: authHeaders(form.getHeaders()),
    timeout: 120000,
  });

  return response.data.file?.id || response.data.id;
}

async function runWorkflow(fileId) {
  const response = await axios.post(
    API + '/v2/runs',
    {
      wrapperCodes: ['customer_support_reply_summary'],
      model: 'openai-gpt-4o-mini',
      input: {
        text: 'Summarize the attached customer feedback and list the top action items.',
        files: [fileId],
      },
      output: {
        format: 'markdown',
        artifact: true,
      },
    },
    {
      headers: authHeaders({
        'Content-Type': 'application/json',
        'Idempotency-Key': crypto.randomUUID(),
      }),
      timeout: 180000,
    }
  );

  return response.data.run;
}

async function downloadArtifact(artifactId, outputPath = './zywrap-output.md') {
  const response = await axios.get(API + '/v2/artifacts/' + artifactId + '/download', {
    headers: authHeaders(),
    responseType: 'stream',
    timeout: 120000,
  });

  await new Promise((resolve, reject) => {
    const writer = fs.createWriteStream(outputPath);
    response.data.pipe(writer);
    writer.on('finish', resolve);
    writer.on('error', reject);
  });

  return outputPath;
}

async function main() {
  if (!API_KEY) throw new Error('Missing ZYWRAP_API_KEY');

  const fileId = await uploadFile('./customer-feedback.csv');
  const run = await runWorkflow(fileId);

  console.log('Run status:', run.status);
  console.log('Output:', run.outputText || run.outputSummary);

  const artifactId = run.artifacts?.[0]?.id;
  if (artifactId) {
    const savedTo = await downloadArtifact(artifactId);
    console.log('Artifact saved to:', savedTo);
  }
}

main().catch((error) => {
  console.error(error.response?.data || error.message);
  process.exit(1);
});
OR

Option 2: Advanced Local Database Sync

Self-host the wrapper data to ensure zero-latency lookups, complete offline capability, and highly customized API integrations.

Step 1: Download Enterprise Catalog SDK

Download the Enterprise-only ZIP file containing compressed catalog metadata. This includes categories, use cases, wrapper codes, input schemas, models, and relations — not prompt logic. Unzip the zywrap-data.json file from the bundle to use in the import script.

Login to check Enterprise Catalog SDK access.

Step 2: Database Setup

Run this SQL. It features the relational use_cases table containing the dynamic schema_data.

sql

-- Database Schema for Zywrap Enterprise Catalog SDK (PostgreSQL)

CREATE TABLE "ai_models" (
  "code" VARCHAR(255) PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "status" BOOLEAN DEFAULT TRUE,
  "ordering" INT
);

CREATE TABLE "categories" (
  "code" VARCHAR(255) PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "status" BOOLEAN DEFAULT TRUE,
  "ordering" INT
);

CREATE TABLE "languages" (
  "code" VARCHAR(10) PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "status" BOOLEAN DEFAULT TRUE,
  "ordering" INT
);

CREATE TABLE "use_cases" (
  "code" VARCHAR(255) PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "description" TEXT,
  "category_code" VARCHAR(255) REFERENCES categories(code) ON DELETE SET NULL,
  "schema_data" JSONB,
  "status" BOOLEAN DEFAULT TRUE,
  "ordering" BIGINT
);

CREATE TABLE "wrappers" (
  "code" VARCHAR(255) PRIMARY KEY,
  "name" VARCHAR(255) NOT NULL,
  "description" TEXT,
  "use_case_code" VARCHAR(255) REFERENCES use_cases(code) ON DELETE SET NULL,
  "featured" BOOLEAN DEFAULT FALSE,
  "base" BOOLEAN DEFAULT FALSE,
  "status" BOOLEAN DEFAULT TRUE,
  "ordering" BIGINT
);

CREATE TABLE "block_templates" (
  "type" VARCHAR(50) NOT NULL,
  "code" VARCHAR(255) NOT NULL,
  "name" VARCHAR(255) NOT NULL,
  "status" BOOLEAN DEFAULT TRUE,
  PRIMARY KEY ("type", "code")
);

CREATE TABLE "settings" (
  "setting_key" VARCHAR(255) PRIMARY KEY,
  "setting_value" TEXT
);

CREATE TABLE "usage_logs" (
  "id" BIGSERIAL PRIMARY KEY,
  "trace_id" VARCHAR(255),
  "wrapper_code" VARCHAR(255),
  "model_code" VARCHAR(255),
  "prompt_tokens" INT DEFAULT 0,
  "completion_tokens" INT DEFAULT 0,
  "total_tokens" INT DEFAULT 0,
  "credits_used" BIGINT DEFAULT 0,
  "latency_ms" INT DEFAULT 0,
  "status" VARCHAR(50) DEFAULT 'success',
  "error_message" TEXT,
  "created_at" TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes for performance
CREATE INDEX idx_usage_wrapper ON usage_logs(wrapper_code);
CREATE INDEX idx_usage_model ON usage_logs(model_code);
CREATE INDEX idx_use_case_cat ON use_cases(category_code);
CREATE INDEX idx_wrapper_uc ON wrappers(use_case_code);

Step 3: Database Connection

Save this as db.js. We recommend using the pg package for PostgreSQL.

javascript

// FILE: db.js
// Uses the popular 'pg' library for PostgreSQL
// npm install pg

const { Pool } = require('pg');

// Replace with your actual database credentials
const pool = new Pool({
  user: 'postgres',
  host: 'localhost',
  database: 'zywrap_db',
  password: 'password',
  port: 5432,
});

pool.on('error', (err, client) => {
  console.error('Unexpected error on idle client', err);
  process.exit(-1);
});

module.exports = {
  query: (text, params) => pool.query(text, params),
  pool
};

Step 4: Import Tabular Data

This script parses the compressed Enterprise catalog JSON and securely bulk-inserts it into your database.

javascript

// FILE: import.js
// USAGE: node import.js
// This script assumes you have 'zywrap-data.json' in the same directory.
// Tip: To prevent memory limits on massive JSON files, run with: node --max-old-space-size=4096 import.js

const fs = require('fs/promises');
const { pool } = require('./db');

// Helper to expand tabular JSON data into arrays of objects
function extractTabular(tabularData) {
    if (!tabularData || !tabularData.cols || !tabularData.data) return [];
    const cols = tabularData.cols;
    return tabularData.data.map(row => {
        const obj = {};
        cols.forEach((col, i) => obj[col] = row[i]);
        return obj;
    });
}

async function main() {
    console.log('Starting lightning-fast v1.0 data import...');

    let data;
    try {
        const jsonFile = await fs.readFile('zywrap-data.json', 'utf8');
        data = JSON.parse(jsonFile);
    } catch (e) {
        console.error('FATAL: zywrap-data.json not found or invalid.', e.message);
        process.exit(1);
    }

    const client = await pool.connect();
    try {
        await client.query('BEGIN');

        console.log('Clearing tables...');
        await client.query('TRUNCATE wrappers, use_cases, categories, languages, block_templates, ai_models, settings RESTART IDENTITY CASCADE');

        // 1. Import Categories
        if (data.categories) {
            for (const c of extractTabular(data.categories)) {
                await client.query(
                    'INSERT INTO categories (code, name, status, ordering) VALUES ($1, $2, TRUE, $3)', 
                    [c.code, c.name, c.ordering ?? 99999]
                );
            }
            console.log('Categories imported successfully.');
        }
        
        // 2. Import Use Cases
        if (data.useCases) {
            for (const uc of extractTabular(data.useCases)) {
                const schemaJson = uc.schema ? JSON.stringify(uc.schema) : null;
                await client.query(
                    'INSERT INTO use_cases (code, name, description, category_code, schema_data, status, ordering) VALUES ($1, $2, $3, $4, $5, TRUE, $6)', 
                    [uc.code, uc.name, uc.desc, uc.cat, schemaJson, uc.ordering ?? 999999999]
                );
            }
            console.log('Use Cases imported successfully.');
        }

        // 3. Import Wrappers
        if (data.wrappers) {
            for (const w of extractTabular(data.wrappers)) {
                await client.query(
                    'INSERT INTO wrappers (code, name, description, use_case_code, featured, base, status, ordering) VALUES ($1, $2, $3, $4, $5, $6, TRUE, $7)',
                    [w.code, w.name, w.desc, w.usecase, !!w.featured, !!w.base, w.ordering ?? 999999999]
                );
            }
            console.log('Wrappers imported successfully.');
        }

        // 4. Import Languages
        if (data.languages) {
            let ord = 1;
            for (const l of extractTabular(data.languages)) {
                await client.query(
                    'INSERT INTO languages (code, name, status, ordering) VALUES ($1, $2, TRUE, $3)', 
                    [l.code, l.name, ord++]
                );
            }
            console.log('Languages imported successfully.');
        }

        // 5. Import AI Models
        if (data.aiModels) {
            for (const m of extractTabular(data.aiModels)) {
                await client.query(
                    'INSERT INTO ai_models (code, name, status, ordering) VALUES ($1, $2, TRUE, $3)',
                    [m.code, m.name, m.ordering ?? 99999]
                );
            }
            console.log('AI Models imported successfully.');
        }

        // 6. Import Block Templates
        if (data.templates) {
            for (const [type, tabular] of Object.entries(data.templates)) {
                for (const tpl of extractTabular(tabular)) {
                    await client.query(
                        'INSERT INTO block_templates (type, code, name, status) VALUES ($1, $2, $3, TRUE)', 
                        [type, tpl.code, tpl.name]
                    );
                }
            }
            console.log('Block templates imported successfully.');
        }

        // 7. Store Version
        if (data.version) {
            await client.query(
                "INSERT INTO settings (setting_key, setting_value) VALUES ('data_version', $1) ON CONFLICT (setting_key) DO UPDATE SET setting_value = EXCLUDED.setting_value",
                [data.version]
            );
            console.log('Data version saved to settings table.');
        }

        await client.query('COMMIT');
        console.log('\nāœ… v1.0 Import complete! Version: ' + (data.version || 'N/A'));

    } catch (e) {
        await client.query('ROLLBACK');
        console.error('FATAL: Database error during import. Transaction rolled back.', e.message);
        process.exit(1);
    } finally {
        client.release();
        pool.end();
    }
}

main();

Step 5: The Dynamic API & Playground

This Express server reads from your local DB, handles dynamic schemas, forwards the final request to the Zywrap API, and logs local usage automatically.

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Zywrap Enterprise Catalog Playground</title>
    <style>
        :root {
            --primary: #2563eb; --primary-hover: #1d4ed8;
            --bg: #f8fafc; --card-bg: #ffffff;
            --text-main: #0f172a; --text-muted: #64748b;
            --border: #e2e8f0; --radius: 0.75rem;
        }
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text-main); line-height: 1.5; padding: 2rem; }
        .container { max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1.8fr; gap: 2rem; }
        @media (max-width: 768px) { .container { grid-template-columns: 1fr; } }
        
        .card { background: var(--card-bg); border-radius: var(--radius); border: 1px solid var(--border); box-shadow: 0 1px 3px rgba(0,0,0,0.05); padding: 1.5rem; margin-bottom: 1.5rem; }
        .card-title { font-size: 1.25rem; font-weight: 700; margin-bottom: 1rem; color: var(--text-main); }
        
        .form-group { margin-bottom: 1rem; }
        label { display: block; font-size: 0.875rem; font-weight: 600; margin-bottom: 0.35rem; color: #334155; }
        
        .input, select, textarea { 
            width: 100%; padding: 0.625rem 0.75rem; border: 1px solid var(--border); 
            border-radius: 0.5rem; font-size: 0.875rem; background: #fff; transition: border-color 0.2s; 
        }
        .input:focus, select:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
        textarea { resize: vertical; min-height: 80px; }
        
        .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
        
        .btn { 
            width: 100%; background: var(--primary); color: white; border: none; padding: 0.875rem; 
            border-radius: 0.5rem; font-weight: 600; font-size: 1rem; cursor: pointer; transition: background 0.2s; 
        }
        .btn:hover { background: var(--primary-hover); }
        .btn:disabled { opacity: 0.7; cursor: not-allowed; }
        
        .schema-section { background: #f8fafc; border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; }
        .schema-title { font-size: 0.875rem; font-weight: 700; border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; margin-bottom: 0.75rem; color: #475569; }
        
        #output { background: #f1f5f9; padding: 1.25rem; border-radius: 0.5rem; font-family: ui-monospace, monospace; font-size: 0.875rem; white-space: pre-wrap; min-height: 300px; border: 1px solid var(--border); overflow-x: auto; }
    </style>
</head>
<body>
    <div class="container">
        <div>
            <div class="card">
                <h2 class="card-title">Configuration</h2>
                
                <div class="form-group">
                    <label>1. Category</label>
                    <select id="category"><option value="">Loading...</option></select>
                </div>
                
                <div class="form-group">
                    <label>2. AI Solution</label>
                    <select id="usecase" disabled><option value="">-- Select Category First --</option></select>
                </div>

                <div class="form-group">
                    <label>3. Configuration Style</label>
                    <select id="wrapper" disabled><option value="">-- Select Solution First --</option></select>
                </div>

                <div class="form-group">
                    <label>4. AI Model</label>
                    <select id="model"><option value="">Loading...</option></select>
                </div>

                <div class="form-group">
                    <label>5. Target Language</label>
                    <select id="language"><option value="">Loading...</option></select>
                </div>
            </div>

            <div class="card">
                <h2 class="card-title" style="font-size: 1rem;">Advanced Overrides</h2>
                <div class="grid-2 overrides">
                    <div><label>Tone</label><select id="toneCode"><option value="">-- Default --</option></select></div>
                    <div><label>Style</label><select id="styleCode"><option value="">-- Default --</option></select></div>
                    <div><label>Formatting</label><select id="formatCode"><option value="">-- Default --</option></select></div>
                    <div><label>Complexity</label><select id="complexityCode"><option value="">-- Default --</option></select></div>
                    <div><label>Length</label><select id="lengthCode"><option value="">-- Default --</option></select></div>
                    <div><label>Audience</label><select id="audienceCode"><option value="">-- Default --</option></select></div>
                    <div><label>Goal</label><select id="responseGoalCode"><option value="">-- Default --</option></select></div>
                    <div><label>Output Type</label><select id="outputCode"><option value="">-- Default --</option></select></div>
                </div>
            </div>
        </div>

        <div>
            <div class="card">
                <div id="dynamic-schema-container"></div>
                
                <div class="form-group">
                    <label id="prompt-label">Prompt / Additional Context</label>
                    <textarea id="prompt" class="input" placeholder="Type your request or additional instructions here..."></textarea>
                </div>
                
                <button id="run-button" class="btn">Generate Response</button>
            </div>

            <div class="card">
                <h2 class="card-title">AI Response</h2>
                <pre id="output">Output will appear here...</pre>
            </div>
        </div>
    </div>

    <script>
        const API_ENDPOINT = 'http://localhost:5000/api';
        
        const elements = {
            category: document.getElementById('category'),
            usecase: document.getElementById('usecase'),
            wrapper: document.getElementById('wrapper'),
            model: document.getElementById('model'),
            language: document.getElementById('language'),
            schemaContainer: document.getElementById('dynamic-schema-container'),
            prompt: document.getElementById('prompt'),
            promptLabel: document.getElementById('prompt-label'),
            runBtn: document.getElementById('run-button'),
            output: document.getElementById('output')
        };

        async function fetchAPI(action, params = '') {
            const res = await fetch(API_ENDPOINT + '?action=' + action + '&' + params);
            return await res.json();
        }

        function populateSelect(el, data, placeholder = '-- Select --') {
            el.innerHTML = '<option value="">' + placeholder + '</option>';
            data.forEach(item => {
                const opt = document.createElement('option');
                opt.value = item.code; 
                opt.textContent = item.name;
                el.appendChild(opt);
            });
            el.disabled = false;
        }

        async function init() {
            const [categories, models, langs, templates] = await Promise.all([
                fetchAPI('get_categories'),
                fetchAPI('get_ai_models'),
                fetchAPI('get_languages'),
                fetchAPI('get_block_templates')
            ]);
            
            populateSelect(elements.category, categories);
            populateSelect(elements.model, models, 'Default Model');
            populateSelect(elements.language, langs, 'English (Default)');

            const overrideMap = {
                tones: 'toneCode', styles: 'styleCode', formattings: 'formatCode',
                complexities: 'complexityCode', lengths: 'lengthCode', audienceLevels: 'audienceCode',
                responseGoals: 'responseGoalCode', outputTypes: 'outputCode'
            };
            for (const [type, elId] of Object.entries(overrideMap)) {
                if (templates[type]) populateSelect(document.getElementById(elId), templates[type], '-- Default --');
            }
        }

        // CASCADING LOGIC 1: Category -> Load Use Cases
        elements.category.addEventListener('change', async () => {
            if (!elements.category.value) {
                elements.usecase.innerHTML = '<option value="">-- Select Category First --</option>';
                elements.usecase.disabled = true;
                elements.wrapper.innerHTML = '<option value="">-- Select Solution First --</option>';
                elements.wrapper.disabled = true;
                elements.schemaContainer.innerHTML = '';
                return;
            }
            elements.usecase.innerHTML = '<option value="">Loading...</option>';
            const useCases = await fetchAPI('get_use_cases', 'category=' + elements.category.value);
            populateSelect(elements.usecase, useCases, '-- Select a Solution --');
            
            elements.wrapper.innerHTML = '<option value="">-- Select Solution First --</option>';
            elements.wrapper.disabled = true;
            elements.schemaContainer.innerHTML = '';
        });

        // CASCADING LOGIC 2: Use Case -> Load Wrappers
        elements.usecase.addEventListener('change', async () => {
            if (!elements.usecase.value) {
                elements.wrapper.innerHTML = '<option value="">-- Select Solution First --</option>';
                elements.wrapper.disabled = true;
                elements.schemaContainer.innerHTML = '';
                return;
            }
            
            elements.wrapper.innerHTML = '<option value="">Loading...</option>';
            let wrappers = await fetchAPI('get_wrappers', 'usecase=' + elements.usecase.value);
            
            elements.wrapper.innerHTML = '<option value="">-- Select a Style --</option>';
            
            let autoSelectCode = null; 

            wrappers.forEach((w, index) => {
                const opt = document.createElement('option');
                opt.value = w.code;
                const parts = w.name.split('—');
                const displayName = w.base ? '✨ Base Template - ' + parts[0].trim() : '↳ Variation: ' + (parts.length > 1 ? parts[1].trim() : w.name);
                opt.textContent = displayName;
                elements.wrapper.appendChild(opt);

                if (w.base) {
                    autoSelectCode = w.code;
                } else if (index === 0 && !autoSelectCode) {
                    autoSelectCode = w.code;
                }
            });
            elements.wrapper.disabled = false;

            if (autoSelectCode) {
                elements.wrapper.value = autoSelectCode;
                elements.wrapper.dispatchEvent(new Event('change')); 
            }
        });

        // CASCADING LOGIC 3: Wrapper -> Load Schema
        elements.wrapper.addEventListener('change', async () => {
            elements.schemaContainer.innerHTML = '';
            elements.promptLabel.textContent = "Prompt / Additional Context";
            if (!elements.wrapper.value) return;

            const schema = await fetchAPI('get_schema', 'wrapper=' + elements.wrapper.value);
            if (!schema || (!schema.req && !schema.opt)) return;

            let html = '';
            elements.promptLabel.textContent = "Additional Free-form Instructions";

            const buildSection = (title, data) => {
                if (!data || Object.keys(data).length === 0) return '';
                let sectionHtml = '<div class="schema-section"><h3 class="schema-title">' + title + '</h3><div class="grid-2">';
                for (const [key, def] of Object.entries(data)) {
                    const isPlaceholder = def.p !== undefined ? def.p : false;
                    const defaultVal = def.d !== undefined ? def.d : '';
                    const placeholderAttr = isPlaceholder ? 'placeholder="' + defaultVal + '"' : '';
                    const valueAttr = (!isPlaceholder && defaultVal) ? 'value="' + defaultVal + '"' : '';
                    const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
                    
                    sectionHtml += '<div><label>' + label + '</label><input type="text" class="input schema-input" data-key="' + key + '" ' + placeholderAttr + ' ' + valueAttr + '></div>';
                }
                return sectionHtml + '</div></div>';
            };

            html += buildSection('Core Inputs', schema.req);
            html += buildSection('Additional Context', schema.opt);
            elements.schemaContainer.innerHTML = html;
        });

        // Execute
        elements.runBtn.addEventListener('click', async () => {
            if (!elements.wrapper.value) return alert("Please select a wrapper.");
            elements.output.textContent = 'Executing...';
            elements.runBtn.disabled = true;
            elements.runBtn.textContent = 'Generating...';
            
            // 1. Grab the exact user prompt
            let finalPrompt = elements.prompt.value.trim();
            
            // 2. Build the variables object natively
            const variables = {};
            document.querySelectorAll('.schema-input').forEach(input => {
                const val = input.value.trim();
                if (val !== '') {
                    variables[input.dataset.key] = val;
                }
            });

            // 3. Build overrides
            const overrides = {};
            document.querySelectorAll('.overrides select').forEach(sel => {
                if (sel.value) overrides[sel.id] = sel.value;
            });

            try {
                const response = await fetch(API_ENDPOINT + '?action=execute', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        model: elements.model.value,
                        wrapperCode: elements.wrapper.value,
                        language: elements.language.value,
                        prompt: finalPrompt, // Only the raw text area
                        variables: variables, // Only the object (backend formats it!)
                        overrides: overrides
                    })
                });
                
                const text = await response.text();
                if (!response.ok) {
                    let errMsg = text;
                    try { errMsg = JSON.parse(text).error || errMsg; } catch(e) {}
                    throw new Error(errMsg);
                }
                
                const data = JSON.parse(text);
                elements.output.textContent = data.output || JSON.stringify(data, null, 2);
            } catch (error) {
                elements.output.textContent = 'Error: ' + error.message;
            } finally {
                elements.runBtn.disabled = false;
                elements.runBtn.textContent = 'Generate Response';
            }
        });

        init();
    </script>
</body>
</html>
javascript

// FILE: server.js
// A simple Express server to replicate the 'api.php' Enterprise catalog playground backend.
//
// REQUIREMENTS:
// npm install express cors pg
// (Requires Node 18+ for native fetch)
//
// USAGE:
// 1. Save this as 'server.js'
// 2. Run: node server.js
// 3. Open 'playground.html' in your browser.

const express = require('express');
const cors = require('cors');
const { pool } = require('./db.js');

const app = express();
app.use(cors());
app.use(express.json());

const ZYWRAP_API_KEY = "YOUR_ZYWRAP_API_KEY";
const ZYWRAP_PROXY_URL = 'https://api.zywrap.com/v1/proxy';

// --- Database Helper Functions ---

async function getCategories() {
    const res = await pool.query("SELECT code, name FROM categories WHERE status = TRUE ORDER BY ordering ASC");
    return res.rows;
}

// šŸš€ NEW: Fetch Solutions (Use Cases) by Category
async function getUseCases(categoryCode) {
    const res = await pool.query(
        "SELECT code, name FROM use_cases WHERE category_code = $1 AND status = TRUE ORDER BY ordering ASC",
        [categoryCode]
    );
    return res.rows;
}

// šŸš€ UPDATED: Fetch Wrappers (Styles) by Use Case
async function getWrappersByUseCase(useCaseCode) {
    const res = await pool.query(
        "SELECT code, name, featured, base FROM wrappers WHERE use_case_code = $1 AND status = TRUE ORDER BY ordering ASC",
        [useCaseCode]
    );
    return res.rows;
}

async function getSchemaByWrapper(wrapperCode) {
    const res = await pool.query(
        "SELECT uc.schema_data FROM use_cases uc JOIN wrappers w ON w.use_case_code = uc.code WHERE w.code = $1 AND w.status = TRUE AND uc.status = TRUE",
        [wrapperCode]
    );
    return res.rows[0] ? res.rows[0].schema_data : null;
}

async function getLanguages() {
    const res = await pool.query("SELECT code, name FROM languages WHERE status = TRUE ORDER BY ordering ASC");
    return res.rows;
}

async function getAiModels() {
    const res = await pool.query("SELECT code, name FROM ai_models WHERE status = TRUE ORDER BY ordering ASC");
    return res.rows;
}

async function getBlockTemplates() {
    const res = await pool.query("SELECT type, code, name FROM block_templates WHERE status = TRUE ORDER BY type, name ASC");
    const grouped = {};
    res.rows.forEach(row => {
        if (!grouped[row.type]) grouped[row.type] = [];
        grouped[row.type].push({ code: row.code, name: row.name });
    });
    return grouped;
}

// āœ… HYBRID PROXY EXECUTION
async function executeZywrapProxy(apiKey, model, wrapperCode, prompt, language, variables = {}, overrides = {}) {
    const payloadData = {
        model: model,
        wrapperCodes: [wrapperCode],
        prompt: prompt,
        variables: variables,
        source: 'node_sdk'
    };
    
    if (language) payloadData.language = language;
    if (Object.keys(overrides).length > 0) Object.assign(payloadData, overrides);
        
    const cleanKey = apiKey.trim();
    
    try {
        const response = await fetch(ZYWRAP_PROXY_URL, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + cleanKey,
                'User-Agent': 'ZywrapNodeSDK/1.1'
            },
            body: JSON.stringify(payloadData)
        });

        if (!response.ok) {
            const errText = await response.text();
            try { return { status: response.status, data: JSON.parse(errText) }; }
            catch (e) { return { status: response.status, data: { error: errText } }; }
        }

        // Parse SSE Stream
        const text = await response.text();
        const lines = text.split('\n');
        let finalJson = null;
        
        for (const line of lines) {
            const trimmed = line.trim();
            if (trimmed.startsWith('data: ')) {
                const jsonStr = trimmed.substring(6);
                try {
                    const data = JSON.parse(jsonStr);
                    if (data && (data.output || data.error)) finalJson = data;
                } catch (e) { /* ignore parse errors on partial streams */ }
            }
        }

        if (finalJson) {
            return { status: finalJson.error ? 400 : 200, data: finalJson };
        } else {
            return { status: 500, data: { error: 'Stream parse failed' } };
        }

    } catch (e) {
        return { status: 500, data: { error: e.message } };
    }
}

// --- API Router ---
app.all('/api', async (req, res) => {
    try {
        const action = req.query.action || (req.body && req.body.action);

        if (req.method === 'GET') {
            if (action === 'get_categories') return res.json(await getCategories());
            if (action === 'get_use_cases') return res.json(await getUseCases(req.query.category));
            if (action === 'get_wrappers') return res.json(await getWrappersByUseCase(req.query.usecase));
            if (action === 'get_languages') return res.json(await getLanguages());
            if (action === 'get_ai_models') return res.json(await getAiModels());
            if (action === 'get_block_templates') return res.json(await getBlockTemplates());
            if (action === 'get_schema') return res.json(await getSchemaByWrapper(req.query.wrapper));
        }

        if (req.method === 'POST') {
            if (action === 'execute') {
                const startTime = Date.now();
                const inputData = req.body;
                
                const executionResult = await executeZywrapProxy(
                    ZYWRAP_API_KEY,
                    inputData.model,
                    inputData.wrapperCode || '',
                    inputData.prompt || '',
                    inputData.language,
                    inputData.variables || {},
                    inputData.overrides || {}
                );
                
                const status = executionResult.status;
                const result = executionResult.data;
                const latencyMs = Date.now() - startTime;

                // Async Logging
                try {
                    const statusText = status === 200 ? 'success' : 'error';
                    const traceId = result.id || null;
                    
                    const usage = result.usage || {};
                    const pTokens = usage.prompt_tokens || 0;
                    const cTokens = usage.completion_tokens || 0;
                    const tTokens = usage.total_tokens || 0;
                    
                    const creditsUsed = (result.cost || {}).credits_used || 0;
                    let errorMessage = statusText === 'error' ? result.error : null;
                    
                    if (errorMessage) {
                        const errMsgStr = String(errorMessage);
                        errorMessage = errMsgStr.length > 255 ? errMsgStr.substring(0, 252) + '...' : errMsgStr;
                    }

                    await pool.query(
                        "INSERT INTO usage_logs (trace_id, wrapper_code, model_code, prompt_tokens, completion_tokens, total_tokens, credits_used, latency_ms, status, error_message) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
                        [traceId, inputData.wrapperCode, inputData.model || 'default', pTokens, cTokens, tTokens, creditsUsed, latencyMs, statusText, errorMessage]
                    );
                } catch (logErr) {
                    console.error("Failed to write to usage_logs:", logErr.message);
                }

                return res.status(status).json(result);
            }
        }
        
        return res.status(400).json({ error: 'Invalid action' });

    } catch (e) {
        return res.status(500).json({ error: e.message });
    }
});

const PORT = 5000;
app.listen(PORT, () => {
    console.log("Zywrap Node SDK Playground backend listening at http://localhost:" + PORT);
});

Step 6: Synchronize Data

Run this script on a cron job. It features automatic unzipping for FULL resets and ON CONFLICT optimized upserts for DELTA updates.

javascript

// FILE: zywrap-sync.js
// USAGE: node zywrap-sync.js
// DEPENDENCIES: npm install axios pg adm-zip

const axios = require('axios');
const fs = require('fs');
const AdmZip = require('adm-zip');
const { execSync } = require('child_process');
const { pool } = require('./db.js'); 

// --- CONFIGURATION ---
const API_KEY = 'YOUR_ZYWRAP_API_KEY_HERE'; 
const API_URL = 'https://api.zywrap.com/v1/sdk/v1/sync';

// --- HELPER FUNCTIONS ---

async function upsertBatch(client, tableName, rows, columns, pk = 'code') {
    if (!rows.length) return;
    
    const BATCH_SIZE = 1000; 
    const colNames = columns.map(c => '"' + c + '"').join(', ');
    const updateCols = columns
        .filter(c => c !== pk && c !== 'type')
        .map(c => '"' + c + '" = EXCLUDED."' + c + '"')
        .join(', ');
    const conflictTarget = pk === 'compound_template' ? '(type, code)' : '("' + pk + '")';

    for (let i = 0; i < rows.length; i += BATCH_SIZE) {
        const chunk = rows.slice(i, i + BATCH_SIZE);
        const values = [];
        const rowPlaceholders = [];
        let counter = 1;

        for (const row of chunk) {
            const rowPh = [];
            for (const cell of row) {
                rowPh.push('$' + counter++);
                values.push(cell);
            }
            rowPlaceholders.push('(' + rowPh.join(', ') + ')');
        }

        const query = "INSERT INTO " + tableName + " (" + colNames + ") VALUES " + rowPlaceholders.join(', ') + " ON CONFLICT " + conflictTarget + " DO UPDATE SET " + updateCols;

        try {
            await client.query(query, values);
        } catch (e) {
            console.error("\n   [!] Error upserting batch in " + tableName + ":", e.message);
            throw e; 
        }
    }
    console.log("   [+] Upserted " + rows.length + " records into " + tableName);
}

async function deleteBatch(client, tableName, ids, pk = 'code') {
    if (!ids.length) return;
    const BATCH_SIZE = 2000;
    for (let i = 0; i < ids.length; i += BATCH_SIZE) {
        const chunk = ids.slice(i, i + BATCH_SIZE);
        await client.query('DELETE FROM "' + tableName + '" WHERE "' + pk + '" = ANY($1)', [chunk]);
    }
    console.log("   [-] Deleted " + ids.length + " records from " + tableName);
}

// --- MAIN ---
(async () => {
    console.log('--- šŸš€ Starting Zywrap Enterprise Catalog Sync ---');
    const client = await pool.connect();
    
    try {
        const verRes = await client.query("SELECT setting_value FROM settings WHERE setting_key = 'data_version'");
        const localVersion = verRes.rows[0]?.setting_value || '';
        console.log("šŸ”¹ Local Version: " + (localVersion || 'None'));

        const response = await axios.get(API_URL, {
            params: { fromVersion: localVersion },
            headers: { 'Authorization': 'Bearer ' + API_KEY }
        });
        
        const json = response.data;
        console.log("šŸ”¹ Sync Mode: " + json.mode);

        if (json.mode === 'FULL_RESET') {
            const zipPath = 'zywrap-data.zip';
            const downloadUrl = json.wrappers.downloadUrl;

            console.log("ā¬‡ļø  Attempting automatic download from Zywrap...");
            
            try {
                const dl = await axios({
                    url: downloadUrl,
                    method: 'GET',
                    responseType: 'arraybuffer',
                    headers: { 'Authorization': 'Bearer ' + API_KEY }
                });

                fs.writeFileSync(zipPath, dl.data);
                const mbSize = (fs.statSync(zipPath).size / (1024 * 1024)).toFixed(2);
                console.log("āœ… Data bundle downloaded successfully (" + mbSize + " MB).");
                
                try {
                    console.log('šŸ“¦ Attempting auto-unzip...');
                    const zip = new AdmZip(zipPath);
                    zip.extractAllTo(__dirname, true);
                    console.log('āœ… Auto-unzip successful. Running import script...');
                    
                    fs.unlinkSync(zipPath);
                    
                    // Run the import script automatically
                    execSync('node import.js', { stdio: 'inherit' });
                    
                } catch (zErr) {
                    console.log("āš ļø Failed to auto-unzip (Check directory permissions).");
                    console.log("\nšŸ‘‰ ACTION REQUIRED:");
                    console.log("   1. Please manually unzip '" + zipPath + "' in this folder.");
                    console.log("   2. Then run: node import.js");
                }

            } catch (dlErr) {
                if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
                console.log("āŒ Automatic download failed. HTTP Status: " + (dlErr.response?.status || 'Unknown'));
            }

        } else if (json.mode === 'DELTA_UPDATE') {
            await client.query('BEGIN');
            const meta = json.metadata || {};

            // Categories
            if (meta.categories) {
                const rows = meta.categories.map(r => [r.code, r.name, r.status ?? true, r.position || r.displayOrder || r.ordering]);
                await upsertBatch(client, 'categories', rows, ['code', 'name', 'status', 'ordering']);
            }

            // Languages
            if (meta.languages) {
                const rows = meta.languages.map(r => [r.code, r.name, r.status ?? true, r.ordering]);
                await upsertBatch(client, 'languages', rows, ['code', 'name', 'status', 'ordering']);
            }

            // AI Models
            if (meta.aiModels) {
                const rows = meta.aiModels.map(r => [r.code, r.name, r.status ?? true, r.displayOrder || r.ordering]);
                await upsertBatch(client, 'ai_models', rows, ['code', 'name', 'status', 'ordering']);
            }

            // Templates
            if (meta.templates) {
                const rows = [];
                for (const [type, items] of Object.entries(meta.templates)) {
                    for (const i of items) rows.push([type, i.code, i.label || i.name, i.status ?? true]);
                }
                await upsertBatch(client, 'block_templates', rows, ['type', 'code', 'name', 'status'], 'compound_template');
            }

            // Use Cases
            if (json.useCases?.upserts?.length) {
                const rows = json.useCases.upserts.map(uc => [
                    uc.code, uc.name, uc.description, uc.categoryCode, 
                    uc.schema ? JSON.stringify(uc.schema) : null, 
                    uc.status ?? true, uc.displayOrder || uc.ordering
                ]);
                await upsertBatch(client, 'use_cases', rows, ['code', 'name', 'description', 'category_code', 'schema_data', 'status', 'ordering']);
            }

            // Wrappers
            if (json.wrappers?.upserts?.length) {
                const rows = json.wrappers.upserts.map(w => [
                    w.code, w.name, w.description, w.useCaseCode || w.categoryCode, 
                    !!(w.featured || w.isFeatured), !!(w.base || w.isBaseWrapper), 
                    w.status ?? true, w.displayOrder || w.ordering
                ]);
                await upsertBatch(client, 'wrappers', rows, ['code', 'name', 'description', 'use_case_code', 'featured', 'base', 'status', 'ordering']);
            }

            // Deletes
            if (json.wrappers?.deletes?.length) await deleteBatch(client, 'wrappers', json.wrappers.deletes);
            if (json.useCases?.deletes?.length) await deleteBatch(client, 'use_cases', json.useCases.deletes);
            
            if (json.newVersion) {
                await client.query("INSERT INTO settings (setting_key, setting_value) VALUES ('data_version', $1) ON CONFLICT (setting_key) DO UPDATE SET setting_value = EXCLUDED.setting_value", [json.newVersion]);
            }
            
            await client.query('COMMIT');
            console.log('āœ… Delta Sync Complete.');
        } else {
            console.log('āœ… No updates needed.');
        }

    } catch (e) {
        if (client) await client.query('ROLLBACK');
        console.error('\nāŒ Sync Error:', e.message);
    } finally {
        if (client) client.release();
        await pool.end();
    }
})();

Programmatically Download

Stream the download directly to disk to prevent memory issues.

javascript

// FILE: download-bundle.js
// USAGE: node download-bundle.js
// REQUIREMENTS: npm install axios adm-zip

const axios = require('axios');
const fs = require('fs');
const AdmZip = require('adm-zip');

// --- CONFIGURATION ---
const ZYWRAP_API_KEY = 'YOUR_API_KEY_HERE';
const API_ENDPOINT = 'https://api.zywrap.com/v1/sdk/v1/download';
const OUTPUT_FILE = './zywrap-data.zip';
// ---------------------

async function downloadBundle() {
  console.log('Downloading latest Enterprise catalog metadata from Zywrap...');
  
  if (!ZYWRAP_API_KEY || ZYWRAP_API_KEY === 'YOUR_API_KEY_HERE') {
      console.error("FATAL: Please replace 'YOUR_API_KEY_HERE' with your actual Zywrap API key.");
      process.exit(1);
  }

  try {
    const response = await axios.get(API_ENDPOINT, {
      headers: { 'Authorization': 'Bearer ' + ZYWRAP_API_KEY },
      responseType: 'stream'
    });

    const writer = fs.createWriteStream(OUTPUT_FILE);
    response.data.pipe(writer);

    return new Promise((resolve, reject) => {
      writer.on('finish', () => {
        console.log("āœ… Data bundle downloaded successfully.");
        
        try {
            console.log('šŸ“¦ Extracting files...');
            const zip = new AdmZip(OUTPUT_FILE);
            zip.extractAllTo(__dirname, true);
            console.log('āœ… Extraction complete.');
            
            // Clean up the zip file
            fs.unlinkSync(OUTPUT_FILE);
            
            console.log("šŸ‘‰ Next Step: Run 'node import.js' to load the data into your database.");
            resolve();
        } catch (zipErr) {
            console.error("āš ļø Failed to extract the zip file automatically.");
            console.log("Please manually unzip '" + OUTPUT_FILE + "' and run 'node import.js'.");
            resolve();
        }
      });
      writer.on('error', reject);
      response.data.on('error', reject);
    });

  } catch (error) {
    if (fs.existsSync(OUTPUT_FILE)) fs.unlinkSync(OUTPUT_FILE);
    console.error("āŒ Error downloading bundle: " + (error.response?.status || error.message));
  }
}

downloadBundle();