Training Workshop

DataMagik
Script Engine

From zero to automation hero

Use arrow keys to navigate • Press S for speaker notes

What You'll Learn

  1. JavaScript Basics — the language your scripts are written in
  2. The Script Editor — your workspace for writing and testing
  3. Debugging with console.log — how to see what's happening
  4. Test Context — simulating real data for safe testing
  5. Core APIs — fetch, credentials, tables, documents, email
  6. Script Schedules — automating scripts on a timer
  7. Real-World Examples — putting it all together
16 hands-on exercises throughout. When you see a yellow "Pause the Video" slide, try it yourself!

Prerequisites

  • A DataMagik account with script engine access
  • At least one document template (for the documents exercises)
  • Familiarity with spreadsheets (we'll use analogies!)
  • No programming experience required
Think of scripts like powerful spreadsheet formulas that can talk to the outside world — fetch data from websites, send emails, and generate documents.

Section 2

JavaScript Basics

The language behind your scripts

Variables — Named Containers

Like naming a cell in a spreadsheet: instead of "A1", you give it a meaningful name.
// Use 'const' for values that don't change
const companyName = "Acme Corp";
const maxRetries = 3;
const isActive = true;

// Use 'let' for values that will change
let attempts = 0;
let status = "pending";

// Update a 'let' variable
attempts = attempts + 1;
status = "processing";
Rule of thumb: Start with const. Switch to let only when you need to reassign it.

Data Types

Primitives

// String — text
const name = "John Doe";

// Number — any number
const price = 29.99;
const quantity = 5;

// Boolean — true or false
const isShipped = false;

// null — intentionally empty
const middleName = null;

// undefined — not yet set
let address;

Spreadsheet Analogy

JS TypeSpreadsheet
StringText cell
NumberNumber cell
BooleanTRUE/FALSE
nullEmpty cell (on purpose)
undefinedCell that was never filled

Objects — Groups of Related Data

Like a row in a spreadsheet — multiple named columns of data about one thing.
// An object groups related values together
const order = {
    orderId: "ORD-1234",
    customer: "Jane Smith",
    total: 149.99,
    shipped: false
};

// Access values with dot notation
console.log(order.customer);    // "Jane Smith"
console.log(order.total);       // 149.99

// Nested objects (like a sub-table)
const order2 = {
    orderId: "ORD-5678",
    customer: {
        name: "Bob Jones",
        email: "bob@example.com"
    }
};
console.log(order2.customer.name);   // "Bob Jones"

Arrays — Lists of Things

Like a column in a spreadsheet — a list of values in order.
// An array is a list
const colors = ["red", "green", "blue"];
const prices = [9.99, 24.50, 3.00];

// Access by position (starts at 0!)
console.log(colors[0]);   // "red"
console.log(colors[2]);   // "blue"

// Useful properties
console.log(colors.length);  // 3

// Add to the end
colors.push("yellow");  // ["red", "green", "blue", "yellow"]

// Array of objects (like a whole spreadsheet!)
const orders = [
    { id: "ORD-1", total: 50.00 },
    { id: "ORD-2", total: 75.00 },
    { id: "ORD-3", total: 120.00 }
];

Array Superpowers: map, filter, forEach

const orders = [
    { id: "ORD-1", total: 50,  status: "shipped" },
    { id: "ORD-2", total: 75,  status: "pending" },
    { id: "ORD-3", total: 120, status: "shipped" },
    { id: "ORD-4", total: 30,  status: "pending" }
];

// filter — keep only items that match a condition
const shipped = orders.filter(o => o.status === "shipped");
// Result: [{ id: "ORD-1", ... }, { id: "ORD-3", ... }]

// map — transform each item
const totals = orders.map(o => o.total);
// Result: [50, 75, 120, 30]

// Chain them! Filter then map
const shippedTotals = orders
    .filter(o => o.status === "shipped")
    .map(o => o.total);
// Result: [50, 120]

// forEach — do something with each item
orders.forEach(o => {
    console.log(`Order ${o.id}: $${o.total}`);
});

Functions — Reusable Recipes

Like a custom formula in your spreadsheet — define it once, use it many times.
// Every DataMagik script needs a main function
function main(context) {
    // Your script logic goes here
    console.log("Script started!");
    return { success: true };
}

// You can create helper functions too
function calculateTotal(items) {
    let total = 0;
    for (const item of items) {
        total = total + item.price * item.quantity;
    }
    return total;
}

// Arrow functions — shorter syntax for simple operations
const double = (x) => x * 2;
const greet = (name) => `Hello, ${name}!`;
Required: Every script must have a function main(context). This is where execution begins.

Control Flow — Making Decisions

function main(context) {
    const order = context.order;

    // if / else — like IF() in spreadsheets
    if (order.total > 100) {
        console.log("Large order — apply discount");
    } else if (order.total > 50) {
        console.log("Medium order");
    } else {
        console.log("Small order");
    }

    // Ternary — one-liner if/else
    const label = order.priority === "rush" ? "URGENT" : "STANDARD";

    // Checking multiple conditions
    if (order.status === "paid" && order.total > 0) {
        console.log("Ready to ship");
    }

    if (order.status === "cancelled" || order.status === "refunded") {
        console.log("No action needed");
    }
}

Loops — Repeating Actions

const items = ["Widget A", "Widget B", "Widget C"];

// for...of — the cleanest way to loop
for (const item of items) {
    console.log(`Processing: ${item}`);
}

// Classic for loop — when you need the index
for (let i = 0; i < items.length; i++) {
    console.log(`Item ${i + 1}: ${items[i]}`);
}

// Looping over object keys
const config = { color: "blue", size: "large", qty: 5 };
for (const [key, value] of Object.entries(config)) {
    console.log(`${key} = ${value}`);
}
// Output: "color = blue", "size = large", "qty = 5"

String Methods & Template Literals

const email = "  John.Doe@Example.COM  ";

// Clean up strings
email.trim()           // "John.Doe@Example.COM"
email.trim().toLowerCase()  // "john.doe@example.com"

// Check contents
"hello world".includes("world")  // true
"hello world".startsWith("hello")  // true

// Split and join
"a,b,c".split(",")        // ["a", "b", "c"]
["a", "b", "c"].join(", ")  // "a, b, c"

// Template literals — embed variables in strings (use backticks!)
const name = "Jane";
const total = 99.50;
const message = `Hello ${name}, your total is $${total}.`;
// "Hello Jane, your total is $99.50."

// Multi-line strings
const html = `
    <h1>Invoice</h1>
    <p>Customer: ${name}</p>
    <p>Total: $${total}</p>
`;

JSON — Data Exchange Format

// JSON looks just like JavaScript objects, but it's text
const jsonText = '{"name": "Widget", "price": 9.99}';

// Parse: text → object (so you can work with it)
const product = JSON.parse(jsonText);
console.log(product.name);  // "Widget"

// Stringify: object → text (to send or log it)
const data = { orderId: "ORD-1", items: ["A", "B"] };
const text = JSON.stringify(data);
// '{"orderId":"ORD-1","items":["A","B"]}'

// Pretty-print for debugging (with indentation)
console.log(JSON.stringify(data, null, 2));
// {
//   "orderId": "ORD-1",
//   "items": ["A", "B"]
// }
When to use: JSON.parse() when you receive text data (from APIs). JSON.stringify() when you need to send data or log objects.
⏸ Pause the Video

Exercise 1: Objects & Properties

In the script editor, create a script that:

  1. Creates an object called product with properties: name, sku, price, inStock
  2. Logs the product name and price using console.log()
  3. Creates a nested dimensions object inside product with width, height, weight
  4. Logs the weight using dot notation
function main(context) {
    // Your code here!

}
Hint: Use console.log(`Product: ${product.name}, Price: $${product.price}`)

Exercise 1: Solution

function main(context) {
    const product = {
        name: "Industrial Widget",
        sku: "WDG-2024-001",
        price: 24.99,
        inStock: true,
        dimensions: {
            width: 10,
            height: 5,
            weight: 2.3
        }
    };

    console.log(`Product: ${product.name}`);
    console.log(`Price: $${product.price}`);
    console.log(`Weight: ${product.dimensions.weight} lbs`);

    return product;
}
⏸ Pause the Video

Exercise 2: Arrays — Filter & Map

Write a script that processes an array of orders:

  1. Create an array of 5 order objects, each with id, customer, total, status (use "shipped", "pending", or "cancelled")
  2. Use filter to get only pending orders
  3. Use map to extract just the customer names from pending orders
  4. Log the count: "X orders pending"
  5. Log each pending customer name
Hint: orders.filter(o => o.status === "pending").map(o => o.customer)

Exercise 2: Solution

function main(context) {
    const orders = [
        { id: "ORD-1", customer: "Alice",   total: 50,  status: "shipped" },
        { id: "ORD-2", customer: "Bob",     total: 75,  status: "pending" },
        { id: "ORD-3", customer: "Charlie", total: 120, status: "shipped" },
        { id: "ORD-4", customer: "Diana",   total: 30,  status: "pending" },
        { id: "ORD-5", customer: "Eve",     total: 90,  status: "cancelled" }
    ];

    const pendingOrders = orders.filter(o => o.status === "pending");
    const pendingNames = pendingOrders.map(o => o.customer);

    console.log(`${pendingOrders.length} orders pending`);
    pendingNames.forEach(name => console.log(`  - ${name}`));

    return { pendingCount: pendingOrders.length, customers: pendingNames };
}

Async/Await — Waiting for Results

Like a VLOOKUP that has to go ask another spreadsheet for data — you need to wait for the answer.
// Many DataMagik APIs need 'await' because they talk to external systems
async function main(context) {
    // fetch talks to the internet — it takes time
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();

    // credentials.get talks to the vault
    const apiKey = await credentials.get("MY_API_KEY");

    // documents.generateSync waits for PDF creation
    const result = await documents.generateSync("Invoice", {
        customer: context.customerName,
        total: context.total
    });

    console.log("All done!");
    return data;
}
Key rules: Add async before function main. Put await before any API call that talks to an external system.

Error Handling — When Things Go Wrong

async function main(context) {
    try {
        // Code that might fail goes in 'try'
        const response = await fetch("https://api.example.com/orders");

        if (!response.ok) {
            throw new Error(`API returned status ${response.status}`);
        }

        const data = await response.json();
        console.log(`Fetched ${data.length} orders`);
        return { success: true, data: data };

    } catch (error) {
        // If anything in 'try' fails, execution jumps here
        console.error(`Something went wrong: ${error.message}`);
        return { success: false, error: error.message };

    } finally {
        // This ALWAYS runs, whether success or failure
        console.log("Script finished");
    }
}
Best practice: Always wrap API calls in try/catch. Return a success flag so callers know what happened.
⏸ Pause the Video

Exercise 3: Async & Error Handling

Write a script that:

  1. Is an async function main(context)
  2. Wraps everything in try/catch
  3. Attempts to JSON.parse() a bad string: "not valid json {"
  4. Catches the error and logs the error message
  5. Returns { success: false, error: error.message } in the catch
Hint: JSON.parse() throws an error when given invalid JSON — your catch block will handle it.

Exercise 3: Solution

async function main(context) {
    try {
        console.log("Attempting to parse JSON...");
        const data = JSON.parse('not valid json {');
        console.log("This line never runs!");
        return { success: true, data: data };
    } catch (error) {
        console.error(`Parse failed: ${error.message}`);
        return { success: false, error: error.message };
    } finally {
        console.log("Exercise 3 complete");
    }
}

Output:

Attempting to parse JSON...
[ERROR] Parse failed: Unexpected token 'o' at position 1
Exercise 3 complete

Section 3

The Script Editor

Your workspace for writing automation

Opening the Script Editor

  • Navigate to DataMagik → Script Engine in the sidebar
  • The editor opens in a full-screen layout
  • Four main panels:
    • Script Library (left) — browse your saved scripts
    • Code Editor (center) — where you write code
    • Settings (right) — script configuration
    • Docs (right) — built-in API reference
The editor uses CodeMirror with JavaScript syntax highlighting, auto-indent, bracket matching, and line numbers.

The Toolbar

ButtonWhat It Does
Create NewStart a brand new script with a name and category
Test RunRun your unsaved code with test context — nothing saved, safe to experiment
SaveSave the current code as a new version
ExecuteRun the saved version of the script
ValidateCheck syntax without running — catches typos and missing brackets
Version HistoryBrowse and restore previous versions of the script
Test Run vs Execute: Test Run uses your current editor code (even unsaved). Execute runs the last saved version.

Creating a New Script

  1. Click Create New in the toolbar
  2. Enter a name — descriptive, like "Daily Order Report"
  3. Select a category — helps organize your script library
  4. The editor opens with a starter template:
function main(context) {
    // Your script logic here

    return { success: true };
}
  • The function main(context) entry point is required
  • The context parameter contains data passed to your script
  • Whatever you return becomes the script's output

The Validate Button

Click Validate before running to catch errors early:

Valid

function main(context) {
    const x = 42;
    return { value: x };
}

✓ Valid JavaScript

Invalid

function main(context) {
    const x = 42
    return { value: x
}

✗ Line 3: Unexpected token }

Validation checks:

  • JavaScript syntax (missing brackets, typos)
  • Presence of function main()
  • Warnings for common issues

Saving & Version History

  • Click Save to create a new version
  • Each save creates a numbered version (v1, v2, v3...)
  • The Version History dropdown shows all versions
  • Click any version to view its code
  • Restore a previous version if your changes caused problems
Good habit: Save before making big changes. That way you can always revert to a working version.

When your code has unsaved changes, a dirty badge appears in the toolbar to remind you.

The Docs Panel

  • Click the Docs tab in the right panel
  • Searchable reference of all available APIs
  • Each entry shows:
    • Function name and signature
    • Description of what it does
    • Parameters with types
    • Return value
    • Usage example
  • Search by keyword: try "email", "fetch", "credential"
Keep the Docs panel open while you code — it's the fastest way to check function signatures without leaving the editor.

Settings Panel

  • Timeout — max execution time in seconds (default: 30)
    • Increase for scripts that call slow APIs or generate large documents
  • Priority — execution queue priority (1-10)
    • Higher = processed sooner when queue is busy
  • Category — organize scripts by purpose
    • e.g., "Reports", "Integrations", "Utilities"
  • Test Input Context — JSON data for test runs (more on this later!)
⏸ Pause the Video

Exercise 4: Hello World Script

In the DataMagik Script Editor:

  1. Click Create New and name it "Training - Hello World"
  2. Write a script that:
    • Reads context.name with a fallback default of "World"
    • Logs a greeting: "Hello, [name]!"
    • Logs the current date and time
    • Returns an object with the greeting
  3. Click Validate, then Save, then Test Run
Hints: Use || for the default value. Use template literals with ${} for the greeting. Use new Date().toLocaleString() for the time.

Exercise 4: Solution

function main(context) {
    const name = context.name || "World";
    console.log(`Hello, ${name}!`);
    console.log(`The time is ${new Date().toLocaleString()}`);
    return { greeting: `Hello, ${name}!` };
}

What's happening here

  • context.name || "World" — the OR operator provides a fallback. If context.name is undefined (no test context set), it defaults to "World"
  • Template literals (backtick strings with ${}) embed variables directly into text — much cleaner than string concatenation
  • return { greeting: ... } — the return value becomes the script's output data, visible in execution history

Expected output:

Hello, World!
The time is 3/24/2026, 9:15:00 AM
⏸ Pause the Video

Exercise 5: Explore the Docs Panel

With the Script Editor open:

  1. Open the Docs panel on the right
  2. Search for fetch — read the function signature and example
  3. Search for credentials — note the credentials.get() method
  4. Search for email — look at the required fields for email.send()
  5. Search for documents — compare generate vs generateSync
Keep a mental note of these APIs — we'll be using all of them in the upcoming exercises.

Exercise 5: What You Should Have Found

Search TermKey Takeaway
fetchfetch(url, options?) — takes a URL and optional config object with method, headers, body. Returns a response with .json() and .text()
credentialscredentials.get("NAME") — retrieves a decrypted secret by its UPPER_SNAKE_CASE name. Always use await
emailemail.send({ to, subject, htmlBody }) — at minimum you need a recipient, subject, and either htmlBody or textBody
documentsgenerateSync waits for the PDF and returns a URL. generate queues it and returns a request ID for polling later
The Docs panel is always available while you code — use it as your first stop when you're unsure about a function's parameters or return value.

Section 4

Debugging with console.log

See what your script is doing

Console Methods

function main(context) {
    console.log("This is an info message");       // Standard output
    console.info("Also an info message");          // Same as log
    console.warn("This is a warning");             // Highlighted as warning
    console.error("This is an error");             // Highlighted as error
    console.debug("Verbose debug info");           // Debug level

    // Log objects clearly
    const order = { id: "ORD-1", total: 99.50, items: ["A", "B"] };
    console.log("Order:", JSON.stringify(order, null, 2));

    // Quick type checking
    console.log("Type of total:", typeof order.total);  // "number"
    console.log("Is array?", Array.isArray(order.items));  // true
}
Pro tip: Use JSON.stringify(obj, null, 2) to pretty-print objects. Without it, you'll just see [object Object].

Debugging Patterns

async function main(context) {
    // Pattern 1: Always log context first
    console.log("Context received:", JSON.stringify(context, null, 2));

    // Pattern 2: Log before and after API calls
    console.log("Fetching orders...");
    const response = await fetch("https://api.example.com/orders");
    console.log("Response status:", response.status);

    // Pattern 3: Log intermediate results
    const data = await response.json();
    console.log(`Received ${data.length} orders`);

    // Pattern 4: Check for expected fields
    if (!context.customerId) {
        console.error("Missing required field: customerId");
        return { success: false, error: "customerId is required" };
    }

    // Pattern 5: Log before returning
    const result = { success: true, count: data.length };
    console.log("Returning:", JSON.stringify(result));
    return result;
}

Reading Execution Output

  • Console logs appear in the output panel after execution
  • For test runs, results often appear inline (within a few seconds)
  • For longer executions, logs stream in real-time via WebSocket
  • Execution history shows:
    • Status: success, failed, timeout
    • Duration in milliseconds
    • All console output
    • Return value (output data)
    • Error messages (if failed)
Check the History panel to review past executions and their full output.
⏸ Pause the Video

Exercise 6: Debug Logging

Create a new script called "Training - Debug Practice" that:

  1. Logs the entire context as pretty-printed JSON
  2. Checks if context.orderId exists — log it if found, warn if missing
  3. Loops through every key in context and logs its name, type, and value
  4. Returns an object listing all field names received

Run it with Test Run and observe the output.

Hints: Use JSON.stringify(obj, null, 2) for pretty-print. Use Object.entries(context) to loop key-value pairs. Use typeof to check types.

Exercise 6: Solution

function main(context) {
    // Step 1: Log the entire context
    console.log("Full context:", JSON.stringify(context, null, 2));

    // Step 2: Check if expected fields exist
    if (context.orderId) {
        console.log("Order ID found:", context.orderId);
    } else {
        console.warn("No orderId in context!");
    }

    // Step 3: Log the type of each field
    for (const [key, value] of Object.entries(context)) {
        console.log(`  ${key}: ${typeof value} = ${JSON.stringify(value)}`);
    }

    return { fieldsReceived: Object.keys(context) };
}

What's happening here

  • JSON.stringify(context, null, 2) — the 2 adds indentation so nested objects are readable in logs
  • console.warn() — shows as a warning level log, visually distinct from console.log()
  • Object.entries() — converts an object to an array of [key, value] pairs so you can loop over them
  • typeof — returns the data type as a string ("string", "number", "object", etc.)
This "log everything first" pattern is the #1 debugging technique. Start every new script this way, then build from there.

Section 5

Test Context

Simulate real data for safe testing

What Is Test Context?

  • When scripts run in production, they receive context data:
    • From automations: screen data, selected rows, user info
    • From schedules: any configured input parameters
    • From other scripts: passed via scripts.run()
  • During development, you don't have that real data
  • Test context lets you provide sample JSON that becomes the context parameter
It's like a spreadsheet's "What-If Analysis" — plug in sample values to see how your formula behaves before going live.

Finding the Test Context Panel

  1. Open the Settings panel (right sidebar)
  2. Scroll down to find Test Input Context
  3. Click to expand the section
  4. You'll see:
    • A text area for entering JSON
    • A Clear button (resets to {})
    • A Validate JSON button (checks your syntax)
The text area has a green text color and monospace font to make it visually distinct from the code editor.

Writing Test Context JSON

{
    "orderId": "ORD-2024-1234",
    "customer": {
        "name": "Jane Smith",
        "email": "jane@example.com",
        "company": "Acme Corp"
    },
    "items": [
        { "sku": "WDG-001", "name": "Widget A", "qty": 2, "price": 24.99 },
        { "sku": "WDG-002", "name": "Widget B", "qty": 1, "price": 49.99 }
    ],
    "shippingMethod": "express",
    "notes": "Handle with care"
}
  • Must be valid JSON (use Validate JSON to check)
  • Structure should match what your script expects
  • This entire object becomes context in your main(context)

Test Run vs Execute

FeatureTest RunExecute
Code usedCurrent editor (even unsaved)Last saved version
Context sourceTest Input Context panelTrigger source (automation, etc.)
Saves a version?NoUses existing version
Priority10 (highest)Normal (5)
Quota usageYes (counts as 1 execution)Yes
ResultsOften inline (fast)Async with polling
Development workflow: Edit code → Update test context → Test Run → Check output → Repeat until it works → Save

Iterating Quickly

The fastest way to develop scripts:

  1. Start simple — just log the context and return it
  2. Set up test context — provide realistic sample data
  3. Test Run — verify context arrives correctly
  4. Add one feature — process one field or make one API call
  5. Test Run again — verify the new code works
  6. Repeat — build up functionality incrementally
  7. Save — only when everything works
Don't write the entire script and then test. Build and test in small increments. Each Test Run costs one execution against your quota, but catching bugs early saves time.

Real-World Context Examples

From an Automation

{
  "screenInfo": {
    "page": "Orders",
    "filters": { "status": "open" }
  },
  "selectedRows": [
    { "id": 101, "customer": "Alice" },
    { "id": 102, "customer": "Bob" }
  ]
}

From a Form Submit

{
  "formData": {
    "customerName": "Charlie",
    "email": "charlie@test.com",
    "quantity": 5,
    "rushOrder": true
  }
}
Look at real execution logs to see what context your automation sends, then copy that structure into your test context.
⏸ Pause the Video

Exercise 7: Test Context in Action

Step 1: Enter this test context JSON in the Settings panel:

{
  "orderId": "ORD-9001",
  "customer": { "name": "Test User", "email": "test@example.com" },
  "items": [
    { "name": "Widget A", "qty": 2, "price": 10.00 },
    { "name": "Widget B", "qty": 1, "price": 25.00 }
  ]
}

Step 2: Write a script that loops through the items, calculates each line total (qty * price), sums them up, and returns the order total.

Hints: Use for...of to loop items. Use let total = 0 and total += lineTotal to accumulate.

Exercise 7: Solution

function main(context) {
    console.log(`Processing order: ${context.orderId}`);
    let total = 0;
    for (const item of context.items) {
        const lineTotal = item.qty * item.price;
        console.log(`  ${item.name}: ${item.qty} x $${item.price} = $${lineTotal}`);
        total += lineTotal;
    }
    console.log(`Order total: $${total}`);
    return { orderId: context.orderId, total: total };
}

What's happening here

  • for (const item of context.items) — loops through each item object in the array. Each item has name, qty, and price
  • let total = 0 — we use let (not const) because the value changes each iteration with +=
  • The return value includes both the orderId and total — this makes the output self-describing so you know which order it belongs to

Expected output:

Processing order: ORD-9001
  Widget A: 2 x $10 = $20
  Widget B: 1 x $25 = $25
Order total: $45
⏸ Pause the Video

Exercise 8: Handling Missing Fields

Using the same script from Exercise 7:

  1. Modify the test context — remove the "items" field entirely
  2. Run the script again — it will crash!
  3. Now update the script to handle missing items gracefully:
    • Default items to an empty array if missing
    • If there are no items, warn and return early with total 0
    • Default each item's qty and price to 0 if missing
Hint: The pattern context.items || [] gives you an empty array if items is undefined.

Exercise 8: Solution

function main(context) {
    console.log(`Processing order: ${context.orderId}`);

    // Handle missing items gracefully
    const items = context.items || [];
    if (items.length === 0) {
        console.warn("No items found in order!");
        return { orderId: context.orderId, total: 0, warning: "No items" };
    }

    let total = 0;
    for (const item of items) {
        const lineTotal = (item.qty || 0) * (item.price || 0);
        console.log(`  ${item.name}: $${lineTotal}`);
        total += lineTotal;
    }
    return { orderId: context.orderId, total: total };
}

What's happening here

  • context.items || [] — if items is undefined, the OR operator substitutes an empty array. This prevents the crash on for...of undefined
  • Early return — if the array is empty, we return immediately with a warning instead of processing an empty loop. This is clearer than silently returning 0
  • (item.qty || 0) — same pattern applied per-field. Even if an individual item is missing qty or price, the math won't produce NaN
Critical takeaway: Real-world data is messy. Always use || defaultValue for any field that might be missing. This single pattern prevents most script crashes.

Section 6

Core Script APIs

The built-in superpowers of your scripts

fetch — HTTP Requests

async function main(context) {
    // Simple GET request
    const response = await fetch("https://api.example.com/orders");
    const data = await response.json();
    console.log(`Received ${data.length} orders`);

    // POST request with body and headers
    const createResponse = await fetch("https://api.example.com/orders", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "Authorization": "Bearer my-token"
        },
        body: JSON.stringify({
            customer: "Jane Smith",
            total: 99.50
        })
    });
    const result = await createResponse.json();
    console.log("Created order:", result.id);
}

fetch — Response Handling

async function main(context) {
    const response = await fetch("https://api.example.com/data");

    // Always check if the request succeeded
    console.log("Status:", response.status);   // 200, 404, 500, etc.
    console.log("OK?", response.ok);           // true if status is 200-299

    if (!response.ok) {
        const errorText = await response.text();
        console.error(`Request failed (${response.status}): ${errorText}`);
        return { success: false, status: response.status };
    }

    // Parse the response body
    const data = await response.json();  // For JSON APIs
    // OR: const text = await response.text();  // For plain text

    return { success: true, data: data };
}
Domain allowlist: Your script can only call domains that your admin has approved. If fetch fails with a domain error, ask your admin to add the domain in Script Engine settings.

http.get — Shorthand

async function main(context) {
    // http.get is a quick shortcut for simple GET requests
    const response = await http.get("https://api.example.com/products");
    const products = await response.json();

    console.log(`Found ${products.length} products`);
    return { products: products };
}
Use http.get() for simple GET requests. Use fetch() when you need to set method, headers, or body.
⏸ Pause the Video

Exercise 9: Fetching External Data

Write an async script that:

  1. Fetches data from https://jsonplaceholder.typicode.com/users
  2. Checks response.ok — throw an error if the request failed
  3. Parses the JSON response with await response.json()
  4. Logs how many users were returned and each user's name + email
  5. Wraps everything in try/catch
Note: Your admin may need to allowlist jsonplaceholder.typicode.com for this exercise.
Hint: Remember the pattern: await fetch(url) → check .okawait .json() → process data.

Exercise 9: Solution

async function main(context) {
    try {
        const response = await fetch(
            "https://jsonplaceholder.typicode.com/users"
        );
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        const users = await response.json();
        console.log(`Fetched ${users.length} users:`);
        for (const user of users) {
            console.log(`  ${user.name} - ${user.email}`);
        }
        return { success: true, userCount: users.length };
    } catch (error) {
        console.error("Fetch failed:", error.message);
        return { success: false, error: error.message };
    }
}

What's happening here

  • await fetch(url) — sends an HTTP GET request and waits for the response. The await is required because network calls take time
  • !response.ok.ok is true for status 200-299. A 404 or 500 does not throw automatically — you must check yourself
  • throw new Error() — jumps to the catch block immediately, just like a crash but controlled
  • { success: true/false } — a consistent return shape so callers always know whether it worked

credentials — Secure Secrets

async function main(context) {
    // Retrieve a stored credential by name (UPPER_SNAKE_CASE)
    const apiKey = await credentials.get("ACME_API_KEY");

    // Use it in an API call
    const response = await fetch("https://api.acme.com/orders", {
        headers: {
            "Authorization": `Bearer ${apiKey}`,
            "Content-Type": "application/json"
        }
    });

    const orders = await response.json();
    return { success: true, orderCount: orders.length };
}
Security rules:
  • NEVER console.log() a credential value
  • NEVER put credentials directly in your code
  • NEVER return credentials in the script output
Credential names use UPPER_SNAKE_CASE: MY_API_KEY, SHIPENGINE, PLEX_AUTH
⏸ Pause the Video

Exercise 10: Using Credentials

Write an async script that:

  1. Retrieves a credential called "MY_API_KEY" using credentials.get()
  2. Checks if the credential was returned — log an error if not
  3. Logs "Credential loaded successfully" (without logging the actual value!)
  4. Uses the credential as a header in a fetch() call
  5. Wraps everything in try/catch
If you don't have a credential set up, create a test one at Script Engine → Credentials first.

Exercise 10: Solution

async function main(context) {
    try {
        const apiKey = await credentials.get("MY_API_KEY");

        if (!apiKey) {
            console.error("Credential MY_API_KEY not found");
            return { success: false, error: "Missing credential" };
        }
        console.log("Credential loaded successfully");

        const response = await fetch("https://api.example.com/data", {
            headers: { "X-API-Key": apiKey }
        });

        console.log("API response status:", response.status);
        return { success: response.ok };
    } catch (error) {
        console.error("Error:", error.message);
        return { success: false, error: error.message };
    }
}

What's happening here

  • credentials.get("MY_API_KEY") — fetches the decrypted value from the secure vault. Uses await because it's an async operation
  • Null check — if the credential name doesn't exist, get() returns undefined. Always check before using
  • Never log the valueconsole.log(apiKey) would expose the secret in execution history. Only confirm it loaded
  • headers: { "X-API-Key": apiKey } — credentials are typically passed as HTTP headers. Common patterns: Authorization: Bearer ${token} or custom headers like X-API-Key

tables — Lookup Tables

async function main(context) {
    // Get all rows from a lookup table
    const allStates = await tables.get("US_STATES");
    console.log(`${allStates.length} states loaded`);

    // Get a specific row by key
    const ohio = await tables.get("US_STATES", "OH");
    console.log("Ohio:", ohio.value);  // "Ohio"

    // Check if a key exists
    const exists = await tables.exists("US_STATES", "XX");
    console.log("XX exists?", exists);  // false

    // Write or update a value
    await tables.set("CONFIG", "last_run", new Date().toISOString());

    // Use as a configuration store
    const emailRecipient = await tables.getValue("CONFIG", "report_email");
    console.log("Send report to:", emailRecipient);
}
Lookup tables are like a named range in a spreadsheet — a simple key-value store you can read and write from scripts.
⏸ Pause the Video

Exercise 11: Working with Lookup Tables

Write a script that demonstrates the full lookup table lifecycle:

  1. Write a value: store "Hello from training!" under key "greeting" in a table called "TRAINING_CONFIG"
  2. Read it back using tables.getValue() and log it
  3. Check if it exists using tables.exists()
  4. Write the current timestamp under key "last_exercise"
You may need to create the TRAINING_CONFIG lookup table first in the DataMagik UI.

Exercise 11: Solution

async function main(context) {
    // Write a config value
    await tables.set("TRAINING_CONFIG", "greeting", "Hello from training!");
    console.log("Config value written");

    // Read it back
    const greeting = await tables.getValue("TRAINING_CONFIG", "greeting");
    console.log("Greeting:", greeting);

    // Check if a key exists
    const hasSetting = await tables.exists("TRAINING_CONFIG", "greeting");
    console.log("Has greeting?", hasSetting);

    // Write a timestamp
    await tables.set("TRAINING_CONFIG", "last_exercise",
        new Date().toISOString());

    return { success: true, greeting: greeting };
}

What's happening here

  • tables.set(table, key, value) — creates or updates a row. If the key already exists, the value is overwritten
  • tables.getValue(table, key) — returns just the value string (vs tables.get() which returns the full row object)
  • tables.exists(table, key) — returns true/false. Useful for branching logic ("if config exists, use it; otherwise use default")
  • Real-world use: lookup tables are great for storing config values, feature flags, and cross-script state that doesn't belong in a database

documents — PDF Generation

async function main(context) {
    // Synchronous generation — waits for the PDF
    const result = await documents.generateSync("Invoice Template", {
        customerName: context.customer.name,
        orderId: context.orderId,
        lineItems: context.items,
        total: context.total
    });

    if (result.success) {
        console.log("PDF URL:", result.url);
        return { success: true, pdfUrl: result.url };
    } else {
        console.error("Generation failed:", result.error);
        return { success: false, error: result.error };
    }
}
Field names in the second parameter must match the merge fields in your document template.

documents — Async Generation

async function main(context) {
    // Async generation — returns immediately with a request ID
    const requestId = await documents.generate("Report Template", {
        title: "Monthly Report",
        data: context.reportData
    });
    console.log("Queued generation, request ID:", requestId);

    // Check status later
    const status = await documents.getStatus(requestId);
    console.log("Status:", status);

    // Options for generateSync
    const result = await documents.generateSync("Label", context.labelData, {
        returnType: "url",       // "url" (default) or "binary"
        timeout: 60000,          // 60 second timeout
        filename: "shipping-label.pdf"
    });
}
Use generateSync when you need the PDF URL immediately. Use generate when you're queuing up many documents and will check status later.
⏸ Pause the Video

Exercise 12: Generating a Document

Set up this test context, then write a script that generates a PDF:

{
  "customer": { "name": "Training User", "email": "test@example.com" },
  "orderId": "TRAIN-001",
  "total": 45.00
}
  1. Use documents.generateSync(templateName, fieldValues)
  2. Pass context data as the field values (must match your template's merge fields)
  3. Check result.success and log the PDF URL
  4. Wrap in try/catch
Replace the template name with an actual template from your account.

Exercise 12: Solution

async function main(context) {
    try {
        console.log(`Generating invoice for ${context.orderId}...`);
        const result = await documents.generateSync("YOUR_TEMPLATE_NAME", {
            customerName: context.customer.name,
            orderId: context.orderId,
            total: "$" + context.total.toFixed(2)
        });
        if (result.success) {
            console.log("PDF ready:", result.url);
        } else {
            console.error("Generation failed:", result.error);
        }
        return result;
    } catch (error) {
        console.error("Doc generation failed:", error.message);
        return { success: false, error: error.message };
    }
}

What's happening here

  • documents.generateSync(name, fields) — sends data to the PDF service and waits until the document is fully rendered. Returns { success, url, error }
  • Field names must match your template — if your template has a merge field called {{customerName}}, the key in the object must be customerName
  • context.total.toFixed(2) — formats the number to 2 decimal places. We prepend "$" since the template expects a formatted string
  • The returned result.url is a downloadable link to the generated PDF, hosted on S3

helpers — Utility Functions

async function main(context) {
    // Safe nested access (no more "cannot read property of undefined")
    const city = helpers.deepGet(context, "customer.address.city", "Unknown");
    console.log("City:", city);  // "Unknown" if path doesn't exist

    // String formatting with tokens
    const msg = helpers.format("Hello {name}, your order {id} is ready!", {
        name: context.customer.name,
        id: context.orderId
    });
    console.log(msg);

    // Padding (great for serial numbers)
    const padded = helpers.pad("42", 6, "0");  // "000042"

    // Generate a unique ID
    const id = helpers.uuid();  // "a1b2c3d4-e5f6-..."

    // Base64 encoding
    const encoded = helpers.base64Encode("Hello World");
    const decoded = helpers.base64Decode(encoded);

    // Parse CSV data
    const csv = "name,age\nAlice,30\nBob,25";
    const rows = helpers.parseCSV(csv);
    // [{ name: "Alice", age: "30" }, { name: "Bob", age: "25" }]
}

email — Sending Email

async function main(context) {
    const result = await email.send({
        to: context.customer.email,
        subject: `Order ${context.orderId} Confirmation`,
        htmlBody: `
            <h2>Thank you for your order!</h2>
            <p>Order ID: ${context.orderId}</p>
            <p>Total: $${context.total.toFixed(2)}</p>
            <p>We'll notify you when it ships.</p>
        `,
        fromName: "Acme Orders",
        replyTo: "support@acme.com"
    });

    if (result.success) {
        console.log("Email sent! Message ID:", result.messageId);
    } else {
        console.error("Email failed:", result.error);
    }
    return result;
}
You can send to multiple recipients: to: ["alice@test.com", "bob@test.com"]
⏸ Pause the Video

Exercise 13: Sending an Email

Set up test context (use your real email so you can verify it arrives):

{
  "recipient": "your-email@example.com",
  "customerName": "Training User",
  "orderId": "TRAIN-001",
  "total": 45.00
}

Write a script that sends a confirmation email with:

  1. The recipient from context
  2. A subject line including the order ID
  3. An HTML body greeting the customer by name with the order total
Hint: email.send({ to, subject, htmlBody }) — use template literals for the HTML body.

Exercise 13: Solution

async function main(context) {
    const result = await email.send({
        to: context.recipient,
        subject: `Order ${context.orderId} - Confirmation`,
        htmlBody: `
            <h2>Hi ${context.customerName},</h2>
            <p>Your order <strong>${context.orderId}</strong>
               for <strong>$${context.total.toFixed(2)}</strong>
               has been confirmed.</p>
            <p>Thank you!</p>
        `
    });
    console.log("Email result:", JSON.stringify(result));
    return result;
}

What's happening here

  • email.send() takes a single options object. The minimum required fields are to, subject, and either htmlBody or textBody
  • Template literals in HTML — backtick strings let you embed ${variables} directly into the HTML body. This is how you create personalized emails
  • <strong> tags — in the HTML body, make key info bold. Recipients scan emails quickly, so highlighting the order ID and total helps
  • The return value includes success and messageId — the message ID can be used to track delivery
Check your inbox (and spam folder). If the email arrived, you've just sent your first automated email from a script!

scripts.run — Composing Scripts

async function main(context) {
    // Call another script by name
    const validationResult = await scripts.run("Validate Order", {
        orderId: context.orderId,
        items: context.items
    });

    if (!validationResult.success) {
        console.error("Validation failed:", validationResult.error);
        return validationResult;
    }

    // Chain multiple scripts
    const enrichedData = await scripts.run("Enrich Customer Data", {
        customerId: context.customerId
    });

    console.log("Customer tier:", enrichedData.tier);
    return { validated: true, customer: enrichedData };
}
Design pattern: Keep scripts small and focused. Use scripts.run() to compose complex workflows from simple building blocks.

Section 7

Other APIs — Quick Reference

More tools in your toolbox

serial — Traceability Numbers

async function main(context) {
    // Generate the next serial in a series
    const serialNum = await serial.next("WORK_ORDER");
    console.log("New serial:", serialNum);  // e.g., "WO-000147"

    // Preview without incrementing
    const preview = await serial.preview("WORK_ORDER");
    console.log("Next would be:", preview);

    // Get series info
    const info = await serial.info("WORK_ORDER");
    console.log("Current count:", info);

    // Generate a batch
    const batch = await serial.batch("SHIPPING_LABEL", 5);
    console.log("Generated 5 serials:", batch);
}
Serial number formats are configured in the DataMagik UI. The series name determines the format and counter.

connectors & zpl — Label Printing

async function main(context) {
    // Print a label from a template
    await connectors.printFromTemplate(
        "Warehouse-Zebra-01",    // Printer name
        "Shipping Label",        // Template name
        {                        // Merge data
            trackingNumber: context.tracking,
            customerName: context.customer.name,
            address: context.shipTo
        },
        2  // Number of copies
    );

    // Or generate ZPL and print raw
    const zplCode = await zpl.fromTemplate("Barcode Label", {
        sku: context.sku,
        description: context.productName
    });
    await connectors.printLabel("Warehouse-Zebra-01", zplCode);

    // List available printers
    const printers = await connectors.getPrinters();
    console.log("Available printers:", JSON.stringify(printers));
}

odbc — External Databases

async function main(context) {
    // Execute a saved query by name
    const result = await odbc.executeQuery("Get Customer Orders", {
        customerId: context.customerId,
        startDate: "2024-01-01"
    });

    if (result.success) {
        console.log(`Found ${result.rowCount} orders`);
        console.log("Columns:", result.columns);
        for (const row of result.rows) {
            console.log(`  Order ${row.OrderID}: $${row.Total}`);
        }
    }

    // Execute with a specific credential
    const erpData = await odbc.executeQuery("ERP Inventory Check", {
        partNumber: context.sku
    }, {
        credential: "ERP_DATABASE",
        timeout: 15
    });
}
ODBC queries are configured in the DataMagik UI with saved connections. Scripts reference them by name.

shipping — ShipEngine Integration

async function main(context) {
    // Get shipping rates
    const rates = await shipping.getRates({
        shipFrom: { postalCode: "43015", countryCode: "US" },
        shipTo: { postalCode: "90210", countryCode: "US" },
        packages: [{ weight: { value: 5, unit: "pound" } }]
    });
    console.log("Cheapest rate:", rates[0]);

    // Validate an address
    const validation = await shipping.validateAddress({
        street: "1 Infinite Loop",
        city: "Cupertino",
        state: "CA",
        postalCode: "95014",
        countryCode: "US"
    });
}

roles & users — Access Management

async function main(context) {
    // List all roles
    const allRoles = await roles.list();
    console.log("Available roles:", allRoles.map(r => r.name));

    // Check if a user has a specific role
    const isAdmin = await users.hasRole(context.userId, "Administrator");
    if (isAdmin) {
        console.log("User is an admin — full access granted");
    }

    // Get user's role list
    const userRoles = await users.getRoles(context.userId);
    console.log("User roles:", userRoles.map(r => r.name));

    // List all users
    const allUsers = await users.list();
    console.log(`${allUsers.length} users in company`);
}
These APIs are useful for scripts that need to check permissions or generate user-specific reports.

Section 8

Script Schedules

Automate scripts on a timer

What Are Script Schedules?

  • Run any saved script automatically at set times
  • Uses cron expressions for flexible scheduling
  • Supports timezones for global teams
  • Built-in retry on failure
  • Enable/disable without deleting
Think of it like scheduling a report in a spreadsheet — "run this every Monday at 9 AM" — but for your scripts.

Navigate to: DataMagik → Script Engine → Schedules

Creating a Schedule

  1. Click Create Schedule
  2. Fill in the form:
    • Schedule Name — e.g., "Daily Order Report"
    • Description — what it does and why
    • Script — select from your saved scripts
    • Cron Expression — when to run (see next slide)
    • Timezone — e.g., "America/New_York"
    • Timeout — max seconds before killing (default: 30)
    • Max Retries — retry count on failure
    • Enabled — toggle on/off
  3. Click Save

Cron Expressions Explained

┌───────── minute (0-59)
│ ┌─────── hour (0-23)
│ │ ┌───── day of month (1-31)
│ │ │ ┌─── month (1-12)
│ │ │ │ ┌─ day of week (0-6, Sun=0)
│ │ │ │ │
* * * * *
ExpressionMeaning
0 9 * * *Every day at 9:00 AM
0 9 * * 1-5Weekdays at 9:00 AM
30 8,17 * * *At 8:30 AM and 5:30 PM daily
0 */2 * * *Every 2 hours
*/15 * * * *Every 15 minutes
0 6 1 * *First day of every month at 6:00 AM
0 0 * * 0Every Sunday at midnight

Timezone Matters

  • Schedules run in the timezone you select
  • Common choices:
    • America/New_York — Eastern Time
    • America/Chicago — Central Time
    • America/Denver — Mountain Time
    • America/Los_Angeles — Pacific Time
    • UTC — Coordinated Universal Time
  • Automatically handles Daylight Saving Time shifts
Common mistake: Setting a schedule for 9 AM but forgetting to set the timezone. If left on UTC, your 9 AM job runs at 4 AM Eastern!

Timeout & Retries

  • Timeout — how long the script is allowed to run
    • Default: 30 seconds
    • Increase for scripts that call slow APIs or generate documents
    • Scripts killed after timeout are marked timeout
  • Max Retries — automatic retry on failure
    • If a script fails, it retries up to this many times
    • Useful for transient network errors
    • Each retry counts as a separate execution
Set timeout to at least 2x your script's expected duration to handle slow days. Set retries to 2-3 for network-dependent scripts.

Monitoring Schedules

  • The schedule list shows:
    • Status badge — Enabled (green) or Disabled (gray)
    • Last run — timestamp and status (success / failed / timeout)
    • Next run — when it will execute next
    • Script name — which script is attached
    • Created by — who set it up
  • Click a schedule to see execution history
  • History shows each run's status, duration, and output
Check schedules regularly. A schedule showing repeated failed status needs investigation!

Enable / Disable

  • Toggle a schedule on or off without deleting it
  • Useful for:
    • Pausing during maintenance windows
    • Temporarily stopping a problematic script
    • Seasonal tasks (enable in Q4, disable in Q1)
  • Disabled schedules keep their configuration and history
  • Re-enable anytime — the next run time recalculates automatically
⏸ Pause the Video

Exercise 14: Create a Schedule

First, make sure you have a saved script (use "Training - Hello World" from Exercise 4).

  1. Go to Script Engine → Schedules
  2. Click Create Schedule
  3. Fill in:
    • Name: Training - Weekday Greeting
    • Script: select your "Training - Hello World" script
    • Cron: 0 9 * * 1-5 (weekdays at 9 AM)
    • Timezone: your local timezone
    • Timeout: 30 seconds • Retries: 1
  4. Save it but leave it disabled (toggle off)
  5. Verify the "Next run" time looks correct

Exercise 14: What You Should See

Breaking down the cron expression

0 9 * * 1-5
│ │ │ │ │
│ │ │ │ └─ Day of week: 1-5 = Monday through Friday
│ │ │ └─── Month: * = every month
│ │ └───── Day of month: * = every day
│ └─────── Hour: 9 = 9 AM
└───────── Minute: 0 = on the hour
  • The Next run should show the next weekday at 9:00 AM in your selected timezone
  • If today is Friday afternoon, the next run should be Monday at 9 AM
  • The schedule appears in the list with a Disabled badge (gray) since we toggled it off
  • If you had left it enabled, the scheduler service would automatically execute your Hello World script every weekday morning
Common cron patterns to memorize: 0 9 * * * (daily 9AM), 0 9 * * 1-5 (weekdays 9AM), */15 * * * * (every 15 min), 0 0 1 * * (monthly)
⏸ Pause the Video

Exercise 15: Review Execution History

If you have any existing schedules with history:

  1. Go to Script Engine → Schedules
  2. Find a schedule that has run at least once
  3. Click it to open the execution history
  4. Look for: status, duration, console output, and error messages
  5. If you see a failed run, read the error and think about what went wrong
If you have no schedule history yet, skip this and come back after your schedules have been running.

Exercise 15: What to Look For

Reading execution history like a pro

ColumnWhat It Tells You
Statussuccess = ran and returned without error. failed = threw an error or returned failure. timeout = exceeded the time limit
DurationHow long the script ran in milliseconds. A sudden spike may indicate an external API slowing down
Console OutputAll console.log/warn/error calls from the run. This is your primary debugging tool
Error MessageIf failed, this shows the exception message. Common: "domain not allowed", "timeout", "credential not found"
Red flag patterns: If you see the same error repeating across multiple runs, the underlying issue needs fixing — retries won't help if the root cause is a misconfiguration.

Section 9

Putting It All Together

Real-world patterns and best practices

Real-World Script: Order Processing

async function main(context) {
    try {
        console.log("=== Order Processing Script ===");
        console.log("Context:", JSON.stringify(context, null, 2));

        // 1. Validate required fields
        if (!context.orderId || !context.customer) {
            throw new Error("Missing orderId or customer in context");
        }

        // 2. Get API credentials
        const apiKey = await credentials.get("ORDER_SYSTEM_KEY");

        // 3. Fetch order details from external system
        console.log(`Fetching order ${context.orderId}...`);
        const response = await fetch(
            `https://api.orders.com/v1/orders/${context.orderId}`, {
            headers: { "Authorization": `Bearer ${apiKey}` }
        });
        if (!response.ok) throw new Error(`API error: ${response.status}`);
        const order = await response.json();
        console.log(`Order total: $${order.total}`);

        // 4. Generate invoice PDF
        console.log("Generating invoice...");
        const doc = await documents.generateSync("Invoice Template", {
            orderId: order.id,
            customerName: context.customer.name,
            lineItems: order.items,
            total: `$${order.total.toFixed(2)}`
        });
        console.log("PDF URL:", doc.url);

        // 5. Email the invoice
        const emailResult = await email.send({
            to: context.customer.email,
            subject: `Invoice for Order ${order.id}`,
            htmlBody: `<p>Please find your invoice attached.</p>
                       <p><a href="${doc.url}">Download Invoice</a></p>`
        });
        console.log("Email sent:", emailResult.success);

        // 6. Update lookup table
        await tables.set("ORDER_STATUS", context.orderId, "invoiced");

        return { success: true, pdfUrl: doc.url, emailSent: emailResult.success };
    } catch (error) {
        console.error("Script failed:", error.message);
        return { success: false, error: error.message };
    }
}

Best Practices

Do

  • Always wrap API calls in try/catch
  • Log at every major step
  • Validate context fields early
  • Use credentials.get() for secrets
  • Return { success: true/false }
  • Use helpers.deepGet() for nested access
  • Set appropriate timeouts
  • Test with realistic test context

Don't

  • Log credential values
  • Hardcode API keys in scripts
  • Skip error handling
  • Write huge scripts — compose small ones
  • Forget to set timezone on schedules
  • Ignore failed execution history
  • Test in production without test runs first
  • Leave schedules enabled while debugging

Common Patterns

PatternApproach
API Integrationcredentials.get() + fetch() + try/catch
Data Transformationcontext.data.map() + .filter() + .reduce()
Document AutomationProcess data → documents.generateSync()email.send()
Config-Driven LogicStore settings in lookup tables, read with tables.get()
Sequential Processingfor...of loop with await inside
Modular ScriptsUtility scripts called via scripts.run()
Label Printingzpl.fromTemplate()connectors.printLabel()
Serial Numbersserial.next() embedded in document fields

Troubleshooting Checklist

  • "Domain not allowed" — Add the domain to your allowlist in Script Engine settings
  • "Credential not found" — Check the credential name is UPPER_SNAKE_CASE and exists
  • "Timeout" — Increase the timeout in script settings or the schedule
  • "Cannot read property of undefined" — A nested field is missing. Use helpers.deepGet() or optional chaining
  • "Quota exceeded" — You've hit your execution limit. Contact your admin
  • Script runs in test but fails in production — Compare your test context to the real context (log it!)
  • Schedule not firing — Check: Is it enabled? Is the cron correct? Is the timezone right?
⏸ Pause the Video

Exercise 16: Build a Complete Script

Your final challenge! Build a script from scratch that:

  1. Receives order data via test context (orderId, customer, items)
  2. Calculates the total from the items array
  3. Looks up the customer's tier from a lookup table
  4. Applies a discount based on tier (Gold: 10%, Silver: 5%, Bronze: 0%)
  5. Logs a summary of the order with the discount applied
  6. Returns the final order summary as the output

Bonus: Add error handling, validate required fields, and handle missing lookup table entries gracefully.

This exercise combines: test context, objects, arrays, loops, lookup tables, and error handling. Take your time!

Exercise 16: Sample Solution

async function main(context) {
    try {
        console.log("=== Order Discount Calculator ===");

        // Validate required fields
        if (!context.orderId) throw new Error("Missing orderId");
        if (!context.customer?.name) throw new Error("Missing customer name");
        if (!context.items || context.items.length === 0) {
            throw new Error("No items in order");
        }

        // Calculate subtotal
        let subtotal = 0;
        for (const item of context.items) {
            const line = (item.qty || 0) * (item.price || 0);
            console.log(`  ${item.name}: ${item.qty} x $${item.price} = $${line}`);
            subtotal += line;
        }
        console.log(`Subtotal: $${subtotal.toFixed(2)}`);

        // Look up customer tier
        let tier = "Bronze";
        try {
            const tierValue = await tables.getValue("CUSTOMER_TIERS", context.customer.name);
            if (tierValue) tier = tierValue;
        } catch (e) {
            console.warn(`Tier lookup failed, defaulting to Bronze: ${e.message}`);
        }

        // Apply discount
        const discounts = { Gold: 0.10, Silver: 0.05, Bronze: 0 };
        const discountRate = discounts[tier] || 0;
        const discount = subtotal * discountRate;
        const total = subtotal - discount;

        console.log(`Customer tier: ${tier} (${discountRate * 100}% off)`);
        console.log(`Discount: -$${discount.toFixed(2)}`);
        console.log(`Total: $${total.toFixed(2)}`);

        return {
            success: true, orderId: context.orderId,
            customer: context.customer.name, tier: tier,
            subtotal, discount, total
        };
    } catch (error) {
        console.error("Error:", error.message);
        return { success: false, error: error.message };
    }
}

Section 10

Wrap-Up

You made it!

What We Covered

SectionKey Takeaway
JavaScript BasicsVariables, objects, arrays, functions, async/await
Script EditorCreate, validate, save, test run, version history
Debuggingconsole.log() everything, especially context
Test ContextSimulate real data in the settings panel
Core APIsfetch, credentials, tables, documents, email, helpers
Other APIsserial, connectors, ODBC, shipping, roles, users
SchedulesCron expressions, timezone, monitoring
Best PracticesError handling, logging, credential security

Next Steps

  • Practice! — Build a real script for your daily work
  • Use the Docs panel — it's always available in the editor
  • Start simple — get one API call working, then expand
  • Review execution history — learn from successes and failures
  • Ask for help — your admin can help with credentials and allowlists

Happy scripting!

DataMagik Script Engine Training • End of Workshop