Training Workshop
From zero to automation hero
Use arrow keys to navigate • Press S for speaker notes
Section 2
The language behind your scripts
// 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";
const. Switch to let only when you need to reassign it.// 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;
| JS Type | Spreadsheet |
|---|---|
| String | Text cell |
| Number | Number cell |
| Boolean | TRUE/FALSE |
| null | Empty cell (on purpose) |
| undefined | Cell that was never filled |
// 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"
// 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 }
];
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}`);
});
// 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}!`;
function main(context). This is where execution begins.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");
}
}
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"
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 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"]
// }
JSON.parse() when you receive text data (from APIs). JSON.stringify() when you need to send data or log objects.In the script editor, create a script that:
product with properties: name, sku, price, inStockconsole.log()dimensions object inside product with width, height, weightfunction main(context) {
// Your code here!
}
console.log(`Product: ${product.name}, Price: $${product.price}`)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;
}
Write a script that processes an array of orders:
id, customer, total, status (use "shipped", "pending", or "cancelled")filter to get only pending ordersmap to extract just the customer names from pending orders"X orders pending"orders.filter(o => o.status === "pending").map(o => o.customer)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 };
}
// 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;
}
async before function main. Put await before any API call that talks to an external system.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");
}
}
success flag so callers know what happened.Write a script that:
async function main(context)try/catchJSON.parse() a bad string: "not valid json {"{ success: false, error: error.message } in the catchJSON.parse() throws an error when given invalid JSON — your catch block will handle it.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
Your workspace for writing automation
| Button | What It Does |
|---|---|
| Create New | Start a brand new script with a name and category |
| Test Run | Run your unsaved code with test context — nothing saved, safe to experiment |
| Save | Save the current code as a new version |
| Execute | Run the saved version of the script |
| Validate | Check syntax without running — catches typos and missing brackets |
| Version History | Browse and restore previous versions of the script |
function main(context) {
// Your script logic here
return { success: true };
}
function main(context) entry point is requiredcontext parameter contains data passed to your scriptreturn becomes the script's outputClick Validate before running to catch errors early:
function main(context) {
const x = 42;
return { value: x };
}
✓ Valid JavaScript
function main(context) {
const x = 42
return { value: x
}
✗ Line 3: Unexpected token }
Validation checks:
function main()When your code has unsaved changes, a dirty badge appears in the toolbar to remind you.
In the DataMagik Script Editor:
context.name with a fallback default of "World""Hello, [name]!"|| for the default value. Use template literals with ${} for the greeting. Use new Date().toLocaleString() for the time.function main(context) {
const name = context.name || "World";
console.log(`Hello, ${name}!`);
console.log(`The time is ${new Date().toLocaleString()}`);
return { greeting: `Hello, ${name}!` };
}
context.name || "World" — the OR operator provides a fallback. If context.name is undefined (no test context set), it defaults to "World"${}) embed variables directly into text — much cleaner than string concatenationreturn { greeting: ... } — the return value becomes the script's output data, visible in execution historyExpected output:
Hello, World!
The time is 3/24/2026, 9:15:00 AM
With the Script Editor open:
fetch — read the function signature and examplecredentials — note the credentials.get() methodemail — look at the required fields for email.send()documents — compare generate vs generateSync| Search Term | Key Takeaway |
|---|---|
fetch | fetch(url, options?) — takes a URL and optional config object with method, headers, body. Returns a response with .json() and .text() |
credentials | credentials.get("NAME") — retrieves a decrypted secret by its UPPER_SNAKE_CASE name. Always use await |
email | email.send({ to, subject, htmlBody }) — at minimum you need a recipient, subject, and either htmlBody or textBody |
documents | generateSync waits for the PDF and returns a URL. generate queues it and returns a request ID for polling later |
Section 4
See what your script is doing
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
}
JSON.stringify(obj, null, 2) to pretty-print objects. Without it, you'll just see [object Object].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;
}
Create a new script called "Training - Debug Practice" that:
context.orderId exists — log it if found, warn if missingRun it with Test Run and observe the output.
JSON.stringify(obj, null, 2) for pretty-print. Use Object.entries(context) to loop key-value pairs. Use typeof to check types.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) };
}
JSON.stringify(context, null, 2) — the 2 adds indentation so nested objects are readable in logsconsole.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 themtypeof — returns the data type as a string ("string", "number", "object", etc.)Section 5
Simulate real data for safe testing
scripts.run()context parameter{}){
"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"
}
context in your main(context)| Feature | Test Run | Execute |
|---|---|---|
| Code used | Current editor (even unsaved) | Last saved version |
| Context source | Test Input Context panel | Trigger source (automation, etc.) |
| Saves a version? | No | Uses existing version |
| Priority | 10 (highest) | Normal (5) |
| Quota usage | Yes (counts as 1 execution) | Yes |
| Results | Often inline (fast) | Async with polling |
The fastest way to develop scripts:
{
"screenInfo": {
"page": "Orders",
"filters": { "status": "open" }
},
"selectedRows": [
{ "id": 101, "customer": "Alice" },
{ "id": 102, "customer": "Bob" }
]
}
{
"formData": {
"customerName": "Charlie",
"email": "charlie@test.com",
"quantity": 5,
"rushOrder": true
}
}
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.
for...of to loop items. Use let total = 0 and total += lineTotal to accumulate.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 };
}
for (const item of context.items) — loops through each item object in the array. Each item has name, qty, and pricelet total = 0 — we use let (not const) because the value changes each iteration with +=Expected output:
Processing order: ORD-9001
Widget A: 2 x $10 = $20
Widget B: 1 x $25 = $25
Order total: $45
Using the same script from Exercise 7:
"items" field entirelyitems to an empty array if missingqty and price to 0 if missingcontext.items || [] gives you an empty array if items is undefined.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 };
}
context.items || [] — if items is undefined, the OR operator substitutes an empty array. This prevents the crash on for...of undefined(item.qty || 0) — same pattern applied per-field. Even if an individual item is missing qty or price, the math won't produce NaN|| defaultValue for any field that might be missing. This single pattern prevents most script crashes.Section 6
The built-in superpowers of your scripts
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);
}
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 };
}
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 };
}
http.get() for simple GET requests. Use fetch() when you need to set method, headers, or body.Write an async script that:
https://jsonplaceholder.typicode.com/usersresponse.ok — throw an error if the request failedawait response.json()try/catchjsonplaceholder.typicode.com for this exercise.await fetch(url) → check .ok → await .json() → process data.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 };
}
}
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 yourselfthrow 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 workedasync 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 };
}
console.log() a credential valueMY_API_KEY, SHIPENGINE, PLEX_AUTHWrite an async script that:
"MY_API_KEY" using credentials.get()"Credential loaded successfully" (without logging the actual value!)fetch() calltry/catchasync 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 };
}
}
credentials.get("MY_API_KEY") — fetches the decrypted value from the secure vault. Uses await because it's an async operationget() returns undefined. Always check before usingconsole.log(apiKey) would expose the secret in execution history. Only confirm it loadedheaders: { "X-API-Key": apiKey } — credentials are typically passed as HTTP headers. Common patterns: Authorization: Bearer ${token} or custom headers like X-API-Keyasync 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);
}
Write a script that demonstrates the full lookup table lifecycle:
"Hello from training!" under key "greeting" in a table called "TRAINING_CONFIG"tables.getValue() and log ittables.exists()"last_exercise"TRAINING_CONFIG lookup table first in the DataMagik UI.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 };
}
tables.set(table, key, value) — creates or updates a row. If the key already exists, the value is overwrittentables.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")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 };
}
}
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"
});
}
generateSync when you need the PDF URL immediately. Use generate when you're queuing up many documents and will check status later.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
}
documents.generateSync(templateName, fieldValues)result.success and log the PDF URLtry/catchasync 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 };
}
}
documents.generateSync(name, fields) — sends data to the PDF service and waits until the document is fully rendered. Returns { success, url, error }{{customerName}}, the key in the object must be customerNamecontext.total.toFixed(2) — formats the number to 2 decimal places. We prepend "$" since the template expects a formatted stringresult.url is a downloadable link to the generated PDF, hosted on S3async 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" }]
}
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;
}
to: ["alice@test.com", "bob@test.com"]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:
email.send({ to, subject, htmlBody }) — use template literals for the HTML body.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;
}
email.send() takes a single options object. The minimum required fields are to, subject, and either htmlBody or textBody${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 helpssuccess and messageId — the message ID can be used to track deliveryasync 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 };
}
scripts.run() to compose complex workflows from simple building blocks.Section 7
More tools in your toolbox
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);
}
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));
}
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
});
}
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"
});
}
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`);
}
Section 8
Automate scripts on a timer
Navigate to: DataMagik → Script Engine → Schedules
┌───────── minute (0-59)
│ ┌─────── hour (0-23)
│ │ ┌───── day of month (1-31)
│ │ │ ┌─── month (1-12)
│ │ │ │ ┌─ day of week (0-6, Sun=0)
│ │ │ │ │
* * * * *
| Expression | Meaning |
|---|---|
0 9 * * * | Every day at 9:00 AM |
0 9 * * 1-5 | Weekdays 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 * * 0 | Every Sunday at midnight |
America/New_York — Eastern TimeAmerica/Chicago — Central TimeAmerica/Denver — Mountain TimeAmerica/Los_Angeles — Pacific TimeUTC — Coordinated Universal TimeFirst, make sure you have a saved script (use "Training - Hello World" from Exercise 4).
Training - Weekday Greeting0 9 * * 1-5 (weekdays at 9 AM)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
0 9 * * * (daily 9AM), 0 9 * * 1-5 (weekdays 9AM), */15 * * * * (every 15 min), 0 0 1 * * (monthly)If you have any existing schedules with history:
| Column | What It Tells You |
|---|---|
| Status | success = ran and returned without error. failed = threw an error or returned failure. timeout = exceeded the time limit |
| Duration | How long the script ran in milliseconds. A sudden spike may indicate an external API slowing down |
| Console Output | All console.log/warn/error calls from the run. This is your primary debugging tool |
| Error Message | If failed, this shows the exception message. Common: "domain not allowed", "timeout", "credential not found" |
Section 9
Real-world patterns and best practices
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 };
}
}
try/catchcredentials.get() for secrets{ success: true/false }helpers.deepGet() for nested access| Pattern | Approach |
|---|---|
| API Integration | credentials.get() + fetch() + try/catch |
| Data Transformation | context.data.map() + .filter() + .reduce() |
| Document Automation | Process data → documents.generateSync() → email.send() |
| Config-Driven Logic | Store settings in lookup tables, read with tables.get() |
| Sequential Processing | for...of loop with await inside |
| Modular Scripts | Utility scripts called via scripts.run() |
| Label Printing | zpl.fromTemplate() → connectors.printLabel() |
| Serial Numbers | serial.next() embedded in document fields |
helpers.deepGet() or optional chainingYour final challenge! Build a script from scratch that:
Bonus: Add error handling, validate required fields, and handle missing lookup table entries gracefully.
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
You made it!
| Section | Key Takeaway |
|---|---|
| JavaScript Basics | Variables, objects, arrays, functions, async/await |
| Script Editor | Create, validate, save, test run, version history |
| Debugging | console.log() everything, especially context |
| Test Context | Simulate real data in the settings panel |
| Core APIs | fetch, credentials, tables, documents, email, helpers |
| Other APIs | serial, connectors, ODBC, shipping, roles, users |
| Schedules | Cron expressions, timezone, monitoring |
| Best Practices | Error handling, logging, credential security |
Happy scripting!
DataMagik Script Engine Training • End of Workshop