AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Unlayer Custom Tools

skill-unlayer-unlayer-skills-unlayer-custom-tools · by unlayer

Builds custom drag-and-drop tools for the Unlayer editor — registering tools, adding property editors, creating custom widgets, head CSS/JS injection, tool configuration, and the custom# prefix convention.

No reviews yet
0 installs
34 views
0.0% view→install

Install

$ agentstack add skill-unlayer-unlayer-skills-unlayer-custom-tools

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-unlayer-unlayer-skills-unlayer-custom-tools)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Unlayer Custom Tools? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Build Custom Tools

Overview

Custom tools are drag-and-drop content blocks you create for the Unlayer editor. Each tool needs:

  • A renderer (what users see in the editor)
  • Exporters (HTML output — must be table-based for email)
  • Property editors (the settings panel)

Complete Example: Product Card

This is a fully working custom tool with an image, title, price, and buy button:

unlayer.registerTool({
  name: 'product_card',
  label: 'Product Card',
  icon: 'fa-shopping-cart',
  supportedDisplayModes: ['web', 'email'],

  options: {
    content: {
      title: 'Content',
      position: 1,
      options: {
        productTitle: {
          label: 'Product Title',
          defaultValue: 'Product Name',
          widget: 'text',               // → values.productTitle = 'Product Name'
        },
        productImage: {
          label: 'Image',
          defaultValue: { url: 'https://via.placeholder.com/300x200' },
          widget: 'image',              // → values.productImage.url = 'https://...'
        },
        price: {
          label: 'Price',
          defaultValue: '$99.99',
          widget: 'text',               // → values.price = '$99.99'
        },
        buttonText: {
          label: 'Button Text',
          defaultValue: 'Buy Now',
          widget: 'text',
        },
        buttonLink: {
          label: 'Button Link',
          defaultValue: { name: 'web', values: { href: 'https://example.com', target: '_blank' } },
          widget: 'link',               // → values.buttonLink.values.href = 'https://...'
        },
      },
    },
    colors: {
      title: 'Colors',
      position: 2,
      options: {
        titleColor: {
          label: 'Title Color',
          defaultValue: '#333333',
          widget: 'color_picker',       // → values.titleColor = '#333333'
        },
        buttonBg: {
          label: 'Button Background',
          defaultValue: '#007bff',
          widget: 'color_picker',
        },
      },
    },
  },

  values: {},

  renderer: {
    Viewer: unlayer.createViewer({
      render(values) {
        return `
          
            
            ${values.productTitle}
            ${values.price}
            
              ${values.buttonText}
            
          
        `;
      },
    }),

    exporters: {
      web(values) {
        return `
          
            
            ${values.productTitle}
            ${values.price}
            
              ${values.buttonText}
            
          
        `;
      },
      email(values) {
        // Email MUST use tables — divs break in Outlook/Gmail
        return `
          
            
              
            
            
              ${values.productTitle}
            
            
              ${values.price}
            
            
              
                
                  
                    ${values.buttonText}
                  
                
              
            
          
        `;
      },
    },

    head: {
      css(values) {
        return `#${values._meta.htmlID} img { max-width: 100%; height: auto; }`;
      },
      js(values) { return ''; },
    },
  },

  validator(data) {
    const { values, defaultErrors } = data;
    const errors = [];
    if (!values.productTitle) {
      errors.push({
        id: 'PRODUCT_TITLE_REQUIRED',
        icon: 'fa-warning',
        severity: 'ERROR',
        title: 'Missing product title',
        description: 'Product title is required',
      });
    }
    if (!values.productImage?.url) {
      errors.push({
        id: 'PRODUCT_IMAGE_REQUIRED',
        icon: 'fa-warning',
        severity: 'ERROR',
        title: 'Missing product image',
        description: 'Product image is required',
      });
    }
    return [...errors, ...defaultErrors];
  },
});

Register it at init time with the custom# prefix:

unlayer.init({
  tools: {
    'custom#product_card': {          // REQUIRED: custom# prefix
      data: {
        apiEndpoint: '/api/products', // Custom data accessible in renderer
      },
      properties: {
        // Override default property values or dropdown options
      },
    },
  },
});

Widget Value Access Reference

How to read each widget type's value in your renderer:

| Widget | Default Value | Access in render(values) | |--------|--------------|---------------------------| | text | 'Hello' | values.myField'Hello' | | rich_text | 'Hello' | values.myField'Hello' | | html | '...' | values.myField'...' | | color_picker | '#FF0000' | values.myField'#FF0000' | | alignment | 'center' | values.myField'center' | | font_family | {label:'Arial', value:'arial'} | values.myField.value'arial' | | image | {url: 'https://...'} | values.myField.url'https://...' | | toggle | false | values.myFieldfalse | | link | {name:'web', values:{href,target}} | values.myField.values.href'https://...' | | counter | '10' | values.myField'10' (string!) | | dropdown | 'option1' | values.myField'option1' | | datetime | '2025-01-01' | values.myField'2025-01-01' | | border | {borderTopWidth:'1px',...} | values.myField.borderTopWidth'1px' |

Dropdown options — pass via unlayer.init() under the tool's properties config:

unlayer.init({
  tools: {
    'custom#product_card': {
      properties: {
        department: {
          editor: {
            data: {
              options: [
                { label: 'Sales', value: 'sales' },
                { label: 'Support', value: 'support' },
              ],
            },
          },
        },
      },
    },
  },
});

Custom Property Editor (React)

For controls beyond built-in widgets:

const RangeSlider = ({ label, value, updateValue, data }) => (
  
    {label}: {value}px
     updateValue(e.target.value + 'px')}
    />
  
);

unlayer.registerPropertyEditor({
  name: 'range_slider',
  Widget: RangeSlider,
});

// Use in your tool:
borderRadius: {
  label: 'Corner Radius',
  defaultValue: '4px',
  widget: 'range_slider',
  data: { min: 0, max: 50 },
},

Validator Return Format

Each error must include id, icon, severity, title, and description:

validator(data) {
  const { values, defaultErrors } = data;
  const errors = [];

  if (!values.productTitle) {
    errors.push({
      id: 'PRODUCT_TITLE_REQUIRED',     // Unique error ID
      icon: 'fa-warning',                // FontAwesome icon
      severity: 'ERROR',                 // 'ERROR' | 'WARNING'
      title: 'Missing product title',    // Short label
      description: 'Product title is required',  // Detailed message
    });
  }

  if (values.price && !values.price.startsWith('$')) {
    errors.push({
      id: 'PRICE_MISSING_CURRENCY',
      icon: 'fa-dollar-sign',
      severity: 'WARNING',
      title: 'Missing currency symbol',
      description: 'Price should include currency symbol',
      labelPath: 'price',               // Optional — highlights the property in the panel
    });
  }

  return [...errors, ...defaultErrors];  // Merge with built-in errors
}

Email-Safe HTML Patterns

Email clients (Outlook, Gmail) require table-based HTML. Copy-paste these patterns:

Button:


  
    
      Button Text
    
  

Two columns:


  
    Left
    Right
  

Safe CSS properties: color, background-color, font-size, font-family, font-weight, text-align, padding, margin, border, width, max-width, display: block/inline-block.

Unsafe (avoid in email): flexbox, grid, position, float, box-shadow, border-radius (partial support), calc().


Common Mistakes

| Mistake | Fix | |---------|-----| | Missing custom# prefix | Tools MUST use custom#my_tool in tools config at init | | Div-based email exporter | Email exporters MUST return table-based HTML | | Forgetting _meta.htmlID | Scope CSS: #${values._meta.htmlID} { ... } | | Hardcoded values in renderer | Use values object — let property editors drive content | | Wrong dropdown options format | Pass options via unlayer.init() under tools['custom#name'].properties.prop.editor.data.options |

Troubleshooting

| Problem | Fix | |---------|-----| | Tool doesn't appear in editor | Check supportedDisplayModes includes current mode | | Properties panel is empty | Check options structure — needs group → options nesting | | Custom editor doesn't update | Ensure updateValue() is called with the new value | | Exported HTML looks different | Check both Viewer.render() and exporters.email/web() |

Resources

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.