Overview
This skill covers advanced features of the MACHHUB SDK including Historian (time-series data), remote function invocation, and workflow execution.
Use this skill when:
- •Querying historical sensor/tag data
- •Analyzing time-series trends
- •Invoking remote functions
- •Executing workflows
- •Building analytics dashboards
Prerequisites:
- •SDK initialized using Designer Extension (zero-config recommended) - see
machhub-sdk-initialization - •For production: Manual configuration - see
machhub-sdk-initializationtemplates
Related Skills:
- •
machhub-sdk-initialization- SDK must be initialized first - •
machhub-sdk-realtime- Real-time tags complement historical data
Historian (Time-Series Data)
What is Historian?
Historian stores all time-series sensor/tag data for historical analysis and trending. Use it for:
- •✅ Time-series data storage
- •✅ Historical queries and analysis
- •✅ Data aggregation (avg, min, max, sum)
- •✅ Trend analysis
Do NOT use Collections for time-series data - use Historian instead.
Basic Historian Query
import { getOrInitializeSDK } from './sdk.service';
const sdk = await getOrInitializeSDK();
const history = await sdk.historian.query({
tagNames: ['temperature/room1'],
startTime: '2024-01-01T00:00:00Z',
endTime: '2024-01-02T00:00:00Z',
interval: '1h',
aggregation: 'avg'
});
console.log(history);
// [
// { timestamp: '2024-01-01T00:00:00Z', value: 22.5 },
// { timestamp: '2024-01-01T01:00:00Z', value: 23.1 },
// ...
// ]
Query Parameters
interface HistorianQuery {
tagNames: string[]; // Tags to query
startTime: string; // ISO 8601 format
endTime: string; // ISO 8601 format
interval?: string; // '1m', '5m', '1h', '1d'
aggregation?: 'avg' | 'min' | 'max' | 'sum' | 'count';
}
Aggregation Types
| Type | Description | Use Case |
|---|---|---|
avg | Average value | Temperature trends |
min | Minimum value | Find lowest readings |
max | Maximum value | Peak detection |
sum | Sum of values | Total production count |
count | Number of readings | Data availability |
Historian Examples
Last 24 Hours Average
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const data = await sdk.historian.query({
tagNames: ['temperature/room1'],
startTime: yesterday.toISOString(),
endTime: now.toISOString(),
interval: '1h',
aggregation: 'avg'
});
Multiple Sensors
const data = await sdk.historian.query({
tagNames: [
'temperature/room1',
'temperature/room2',
'temperature/room3'
],
startTime: '2024-01-01T00:00:00Z',
endTime: '2024-01-02T00:00:00Z',
interval: '30m',
aggregation: 'avg'
});
Production Metrics
// Get hourly production counts
const production = await sdk.historian.query({
tagNames: ['production/line1/count'],
startTime: '2024-01-01T08:00:00Z',
endTime: '2024-01-01T17:00:00Z',
interval: '1h',
aggregation: 'sum'
});
Historian Service Example
// services/analytics.service.ts
import { getOrInitializeSDK } from './sdk.service';
interface TrendData {
timestamp: string;
value: number;
}
class AnalyticsService {
async getProductionTrends(
startDate: Date,
endDate: Date
): Promise<TrendData[]> {
try {
const sdk = await getOrInitializeSDK();
const data = await sdk.historian.query({
tagNames: ['production/rate'],
startTime: startDate.toISOString(),
endTime: endDate.toISOString(),
interval: '1h',
aggregation: 'avg'
});
return data;
} catch (error) {
console.error('Error fetching production trends:', error);
throw error;
}
}
async getTemperatureStats(
sensor: string,
hours: number = 24
) {
try {
const sdk = await getOrInitializeSDK();
const now = new Date();
const start = new Date(now.getTime() - hours * 60 * 60 * 1000);
// Get average
const avgData = await sdk.historian.query({
tagNames: [sensor],
startTime: start.toISOString(),
endTime: now.toISOString(),
interval: '1h',
aggregation: 'avg'
});
// Get min
const minData = await sdk.historian.query({
tagNames: [sensor],
startTime: start.toISOString(),
endTime: now.toISOString(),
interval: '1h',
aggregation: 'min'
});
// Get max
const maxData = await sdk.historian.query({
tagNames: [sensor],
startTime: start.toISOString(),
endTime: now.toISOString(),
interval: '1h',
aggregation: 'max'
});
const average = avgData.reduce((sum, d) => sum + d.value, 0) / avgData.length;
const minimum = Math.min(...minData.map(d => d.value));
const maximum = Math.max(...maxData.map(d => d.value));
return { average, minimum, maximum, data: avgData };
} catch (error) {
console.error('Error fetching temperature stats:', error);
throw error;
}
}
async compareMultipleSensors(
sensors: string[],
hours: number = 24
) {
try {
const sdk = await getOrInitializeSDK();
const now = new Date();
const start = new Date(now.getTime() - hours * 60 * 60 * 1000);
const data = await sdk.historian.query({
tagNames: sensors,
startTime: start.toISOString(),
endTime: now.toISOString(),
interval: '30m',
aggregation: 'avg'
});
return data;
} catch (error) {
console.error('Error comparing sensors:', error);
throw error;
}
}
}
export const analyticsService = new AnalyticsService();
Remote Functions
Invoke Function
const sdk = await getOrInitializeSDK();
// Invoke remote function with parameters
const result = await sdk.function.invoke('functionName', {
param1: 'value1',
param2: 123,
param3: true
});
console.log('Function result:', result);
Function Examples
// Send email notification
await sdk.function.invoke('sendEmail', {
to: 'user@example.com',
subject: 'Alert',
body: 'Temperature exceeded threshold'
});
// Process data
const processed = await sdk.function.invoke('processData', {
source: 'sensors',
operation: 'aggregate',
timeRange: '1h'
});
// Trigger action
await sdk.function.invoke('triggerAction', {
action: 'restart_service',
service: 'data_collector'
});
Workflows (Flows)
Execute Workflow
const sdk = await getOrInitializeSDK();
// Execute workflow with input data
const result = await sdk.flow.execute('workflowName', {
input: 'data',
config: {
mode: 'production',
notify: true
}
});
console.log('Workflow result:', result);
Workflow Examples
// Data processing workflow
const result = await sdk.flow.execute('data_processing_flow', {
source: 'sensor_readings',
destination: 'processed_data',
filters: {
quality: 'good',
status: 'active'
}
});
// Report generation workflow
await sdk.flow.execute('generate_report', {
reportType: 'daily',
format: 'pdf',
recipients: ['manager@example.com']
});
// Automated maintenance workflow
await sdk.flow.execute('maintenance_check', {
equipment: ['machine1', 'machine2'],
checkType: 'preventive'
});
Workflow Service Example
// services/workflow.service.ts
import { getOrInitializeSDK } from './sdk.service';
class WorkflowService {
async processData(
sourceCollection: string,
destinationCollection: string,
filters?: any
) {
try {
const sdk = await getOrInitializeSDK();
return await sdk.flow.execute('data_processing_flow', {
source: sourceCollection,
destination: destinationCollection,
filters: filters || {}
});
} catch (error) {
console.error('Error processing data:', error);
throw error;
}
}
async generateReport(
reportType: string,
parameters: any
) {
try {
const sdk = await getOrInitializeSDK();
return await sdk.flow.execute('generate_report', {
reportType,
...parameters
});
} catch (error) {
console.error('Error generating report:', error);
throw error;
}
}
async invokeFunction(
functionName: string,
parameters: any
) {
try {
const sdk = await getOrInitializeSDK();
return await sdk.function.invoke(functionName, parameters);
} catch (error) {
console.error(`Error invoking function ${functionName}:`, error);
throw error;
}
}
}
export const workflowService = new WorkflowService();
Complete Analytics Example
// Dashboard with historical data
import { analyticsService } from './services';
let temperatureData = [];
let stats = null;
async function loadDashboardData() {
// Get last 24 hours of temperature data
const data = await analyticsService.getProductionTrends(
new Date(Date.now() - 24 * 60 * 60 * 1000),
new Date()
);
temperatureData = data;
// Get statistics
stats = await analyticsService.getTemperatureStats(
'temperature/room1',
24
);
// Update UI with data
console.log('Dashboard data loaded:', { temperatureData, stats });
}
// Call on page load
loadDashboardData();
Best Practices
Historian
- •✅ Use for time-series - Don't store all sensor data in Collections
- •✅ Choose appropriate intervals - Balance detail vs performance
- •✅ Limit time ranges - Query only what you need
- •✅ Use aggregation - Reduce data volume for large ranges
- •✅ Cache results - Store frequently accessed historical data
Functions & Workflows
- •✅ Error handling - Wrap invocations in try-catch
- •✅ Parameter validation - Validate inputs before invoking
- •✅ Timeout handling - Consider long-running operations
- •✅ Logging - Log function/workflow executions
- •✅ Idempotency - Design functions to be safely retriable
Templates
Template 1: Historian Service
File: src/services/historian.service.ts
Purpose: Service for querying time-series data with aggregations
Code:
// filepath: src/services/historian.service.ts
import { getOrInitializeSDK } from './sdk.service';
import type { SDK } from '@machhub-dev/sdk-ts';
export type AggregationType = 'avg' | 'min' | 'max' | 'sum' | 'count';
export interface HistorianQuery {
tagName: string;
startTime: Date;
endTime: Date;
aggregation?: AggregationType;
interval?: string; // e.g., '1h', '15m', '1d'
}
export interface HistorianDataPoint {
timestamp: Date;
value: number;
quality?: 'good' | 'bad' | 'uncertain';
}
class HistorianService {
private sdk: SDK | null = null;
private async getSDK(): Promise<SDK> {
if (!this.sdk) {
this.sdk = await getOrInitializeSDK();
}
return this.sdk;
}
/**
* Query historian data
*/
async query(options: HistorianQuery): Promise<HistorianDataPoint[]> {
try {
const sdk = await this.getSDK();
let query = sdk.historian
.tag(options.tagName)
.from(options.startTime)
.to(options.endTime);
if (options.aggregation) {
query = query.aggregate(options.aggregation, options.interval || '1h');
}
const results = await query.execute();
return results.map((point: any) => ({
timestamp: new Date(point.timestamp),
value: point.value,
quality: point.quality || 'good'
}));
} catch (error) {
console.error('Historian query failed:', error);
throw error;
}
}
/**
* Get average value over time period
*/
async getAverage(
tagName: string,
startTime: Date,
endTime: Date,
interval: string = '1h'
): Promise<HistorianDataPoint[]> {
return await this.query({
tagName,
startTime,
endTime,
aggregation: 'avg',
interval
});
}
/**
* Get min/max values
*/
async getMinMax(
tagName: string,
startTime: Date,
endTime: Date
): Promise<{ min: number; max: number }> {
const [minData, maxData] = await Promise.all([
this.query({
tagName,
startTime,
endTime,
aggregation: 'min'
}),
this.query({
tagName,
startTime,
endTime,
aggregation: 'max'
})
]);
return {
min: minData.length > 0 ? minData[0].value : 0,
max: maxData.length > 0 ? maxData[0].value : 0
};
}
/**
* Get data for the last N hours
*/
async getLastHours(
tagName: string,
hours: number,
aggregation?: AggregationType,
interval?: string
): Promise<HistorianDataPoint[]> {
const endTime = new Date();
const startTime = new Date(endTime.getTime() - hours * 60 * 60 * 1000);
return await this.query({
tagName,
startTime,
endTime,
aggregation,
interval
});
}
/**
* Get data for today
*/
async getToday(
tagName: string,
aggregation?: AggregationType
): Promise<HistorianDataPoint[]> {
const now = new Date();
const startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const endTime = new Date(startTime.getTime() + 24 * 60 * 60 * 1000);
return await this.query({
tagName,
startTime,
endTime,
aggregation,
interval: '1h'
});
}
/**
* Compare two time periods
*/
async comparePeriods(
tagName: string,
period1Start: Date,
period1End: Date,
period2Start: Date,
period2End: Date,
aggregation: AggregationType = 'avg'
): Promise<{
period1: HistorianDataPoint[];
period2: HistorianDataPoint[];
}> {
const [period1, period2] = await Promise.all([
this.query({
tagName,
startTime: period1Start,
endTime: period1End,
aggregation
}),
this.query({
tagName,
startTime: period2Start,
endTime: period2End,
aggregation
})
]);
return { period1, period2 };
}
}
export const historianService = new HistorianService();
Usage:
import { historianService } from './services/historian.service';
// Get average temperature for last 24 hours
const data = await historianService.getLastHours(
'temperature/room1',
24,
'avg',
'1h'
);
// Get today's data
const todayData = await historianService.getToday('temperature/room1', 'avg');
// Get min/max values
const { min, max } = await historianService.getMinMax(
'temperature/room1',
new Date('2024-01-01'),
new Date('2024-01-31')
);
// Compare two weeks
const comparison = await historianService.comparePeriods(
'temperature/room1',
new Date('2024-01-01'),
new Date('2024-01-07'),
new Date('2024-01-08'),
new Date('2024-01-14'),
'avg'
);
Template 2: Function Invocation Service
File: src/services/function.service.ts
Purpose: Service for invoking MACHHUB functions and workflows
Code:
// filepath: src/services/function.service.ts
import { getOrInitializeSDK } from './sdk.service';
import type { SDK } from '@machhub-dev/sdk-ts';
export interface FunctionParams {
[key: string]: any;
}
export interface FunctionResult {
success: boolean;
result?: any;
error?: string;
executionTime?: number;
}
class FunctionService {
private sdk: SDK | null = null;
private async getSDK(): Promise<SDK> {
if (!this.sdk) {
this.sdk = await getOrInitializeSDK();
}
return this.sdk;
}
/**
* Invoke a function
*/
async invoke(
functionName: string,
params: FunctionParams = {}
): Promise<FunctionResult> {
const startTime = Date.now();
try {
// Validate parameters
this.validateParams(params);
const sdk = await this.getSDK();
const result = await sdk.function.invoke(functionName, params);
return {
success: true,
result,
executionTime: Date.now() - startTime
};
} catch (error: any) {
console.error(`Function ${functionName} failed:`, error);
return {
success: false,
error: error.message || 'Function execution failed',
executionTime: Date.now() - startTime
};
}
}
/**
* Invoke workflow
*/
async invokeWorkflow(
workflowName: string,
inputs: FunctionParams = {}
): Promise<FunctionResult> {
const startTime = Date.now();
try {
this.validateParams(inputs);
const sdk = await this.getSDK();
const result = await sdk.workflow.invoke(workflowName, inputs);
return {
success: true,
result,
executionTime: Date.now() - startTime
};
} catch (error: any) {
console.error(`Workflow ${workflowName} failed:`, error);
return {
success: false,
error: error.message || 'Workflow execution failed',
executionTime: Date.now() - startTime
};
}
}
/**
* Invoke function with retry logic
*/
async invokeWithRetry(
functionName: string,
params: FunctionParams = {},
maxRetries: number = 3,
retryDelay: number = 1000
): Promise<FunctionResult> {
let lastError: any;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await this.invoke(functionName, params);
if (result.success) {
return result;
}
lastError = result.error;
} catch (error) {
lastError = error;
}
if (attempt < maxRetries) {
await this.delay(retryDelay * attempt);
}
}
return {
success: false,
error: `Failed after ${maxRetries} attempts: ${lastError}`
};
}
/**
* Invoke multiple functions in parallel
*/
async invokeParallel(
functions: Array<{ name: string; params?: FunctionParams }>
): Promise<FunctionResult[]> {
const promises = functions.map(({ name, params }) =>
this.invoke(name, params)
);
return await Promise.all(promises);
}
/**
* Invoke functions in sequence
*/
async invokeSequential(
functions: Array<{ name: string; params?: FunctionParams }>
): Promise<FunctionResult[]> {
const results: FunctionResult[] = [];
for (const { name, params } of functions) {
const result = await this.invoke(name, params);
results.push(result);
// Stop if any function fails
if (!result.success) {
break;
}
}
return results;
}
/**
* Get function list
*/
async getFunctions(): Promise<string[]> {
try {
const sdk = await this.getSDK();
return await sdk.function.list();
} catch (error) {
console.error('Failed to get functions:', error);
return [];
}
}
/**
* Get workflow list
*/
async getWorkflows(): Promise<string[]> {
try {
const sdk = await this.getSDK();
return await sdk.workflow.list();
} catch (error) {
console.error('Failed to get workflows:', error);
return [];
}
}
/**
* Validate parameters
*/
private validateParams(params: FunctionParams): void {
if (typeof params !== 'object' || params === null) {
throw new Error('Parameters must be an object');
}
// Add custom validation logic here
}
/**
* Delay helper
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
export const functionService = new FunctionService();
Usage:
import { functionService } from './services/function.service';
// Invoke function
const result = await functionService.invoke('calculateTotal', {
items: [10, 20, 30]
});
if (result.success) {
console.log('Result:', result.result);
console.log('Execution time:', result.executionTime, 'ms');
}
// Invoke workflow
const workflowResult = await functionService.invokeWorkflow('processOrder', {
orderId: 'order-123',
customerId: 'customer-456'
});
// Invoke with retry
const retryResult = await functionService.invokeWithRetry(
'unreliableFunction',
{ data: 'test' },
3, // max retries
1000 // delay between retries
);
// Invoke multiple functions in parallel
const parallelResults = await functionService.invokeParallel([
{ name: 'function1', params: { a: 1 } },
{ name: 'function2', params: { b: 2 } },
{ name: 'function3', params: { c: 3 } }
]);
Template 3: Cache Service
File: src/services/cache.service.ts
Purpose: Simple in-memory cache for API results
Code:
// filepath: src/services/cache.service.ts
export interface CacheOptions {
ttl?: number; // Time to live in milliseconds
maxSize?: number; // Maximum number of entries
}
interface CacheEntry<T> {
value: T;
expires: number;
}
class CacheService {
private cache = new Map<string, CacheEntry<any>>();
private defaultTTL = 5 * 60 * 1000; // 5 minutes
private maxSize = 100;
constructor(options: CacheOptions = {}) {
if (options.ttl) this.defaultTTL = options.ttl;
if (options.maxSize) this.maxSize = options.maxSize;
}
/**
* Get value from cache
*/
get<T>(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
// Check if expired
if (Date.now() > entry.expires) {
this.cache.delete(key);
return null;
}
return entry.value as T;
}
/**
* Set value in cache
*/
set<T>(key: string, value: T, ttl?: number): void {
// Enforce max size
if (this.cache.size >= this.maxSize) {
// Remove oldest entry
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
const expires = Date.now() + (ttl || this.defaultTTL);
this.cache.set(key, {
value,
expires
});
}
/**
* Delete value from cache
*/
delete(key: string): void {
this.cache.delete(key);
}
/**
* Clear entire cache
*/
clear(): void {
this.cache.clear();
}
/**
* Check if key exists and is not expired
*/
has(key: string): boolean {
const value = this.get(key);
return value !== null;
}
/**
* Get or set (fetch if not in cache)
*/
async getOrSet<T>(
key: string,
fetcher: () => Promise<T>,
ttl?: number
): Promise<T> {
const cached = this.get<T>(key);
if (cached !== null) {
return cached;
}
const value = await fetcher();
this.set(key, value, ttl);
return value;
}
/**
* Create cache key from parts
*/
static createKey(...parts: (string | number)[]): string {
return parts.join(':');
}
/**
* Get cache statistics
*/
getStats(): { size: number; maxSize: number; keys: string[] } {
return {
size: this.cache.size,
maxSize: this.maxSize,
keys: Array.from(this.cache.keys())
};
}
/**
* Clean expired entries
*/
cleanup(): void {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expires) {
this.cache.delete(key);
}
}
}
}
export const cacheService = new CacheService({
ttl: 5 * 60 * 1000, // 5 minutes
maxSize: 100
});
// Run cleanup every minute
setInterval(() => cacheService.cleanup(), 60 * 1000);
Usage:
import { cacheService, CacheService } from './services/cache.service';
import { productService } from './services/product.service';
// Simple get/set
cacheService.set('user:123', { name: 'John' }, 10 * 60 * 1000); // 10 min TTL
const user = cacheService.get('user:123');
// Get or fetch
const products = await cacheService.getOrSet(
'products:all',
() => productService.getAllProducts(),
5 * 60 * 1000 // 5 minutes
);
// Create cache key
const key = CacheService.createKey('product', productId, 'details');
// Manual delete
cacheService.delete(key);
// Clear all
cacheService.clear();
// Stats
const stats = cacheService.getStats();
console.log('Cache size:', stats.size);
Template 4: Data Transformation Utilities
File: src/utils/data-transform.ts
Purpose: Utilities for transforming and formatting data
Code:
// filepath: src/utils/data-transform.ts
export class DataTransform {
/**
* Group array by key
*/
static groupBy<T>(array: T[], key: keyof T): Record<string, T[]> {
return array.reduce((result, item) => {
const groupKey = String(item[key]);
if (!result[groupKey]) {
result[groupKey] = [];
}
result[groupKey].push(item);
return result;
}, {} as Record<string, T[]>);
}
/**
* Sort array by key
*/
static sortBy<T>(
array: T[],
key: keyof T,
direction: 'asc' | 'desc' = 'asc'
): T[] {
return [...array].sort((a, b) => {
const aVal = a[key];
const bVal = b[key];
if (aVal < bVal) return direction === 'asc' ? -1 : 1;
if (aVal > bVal) return direction === 'asc' ? 1 : -1;
return 0;
});
}
/**
* Filter array by multiple conditions
*/
static filterBy<T>(
array: T[],
filters: Partial<Record<keyof T, any>>
): T[] {
return array.filter(item => {
return Object.entries(filters).every(([key, value]) => {
return item[key as keyof T] === value;
});
});
}
/**
* Map array to key-value object
*/
static toMap<T>(array: T[], key: keyof T): Record<string, T> {
return array.reduce((result, item) => {
result[String(item[key])] = item;
return result;
}, {} as Record<string, T>);
}
/**
* Paginate array
*/
static paginate<T>(
array: T[],
page: number,
limit: number
): { data: T[]; total: number; page: number; pages: number } {
const total = array.length;
const pages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const end = start + limit;
const data = array.slice(start, end);
return { data, total, page, pages };
}
/**
* Calculate statistics for numeric array
*/
static stats(numbers: number[]): {
min: number;
max: number;
avg: number;
sum: number;
count: number;
} {
if (numbers.length === 0) {
return { min: 0, max: 0, avg: 0, sum: 0, count: 0 };
}
const sum = numbers.reduce((acc, val) => acc + val, 0);
const avg = sum / numbers.length;
const min = Math.min(...numbers);
const max = Math.max(...numbers);
return { min, max, avg, sum, count: numbers.length };
}
/**
* Flatten nested object
*/
static flatten(
obj: any,
prefix: string = ''
): Record<string, any> {
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.assign(result, this.flatten(value, newKey));
} else {
result[newKey] = value;
}
}
return result;
}
/**
* Deep clone object
*/
static clone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
/**
* Merge objects deeply
*/
static merge<T>(...objects: Partial<T>[]): T {
return objects.reduce((result, obj) => {
return { ...result, ...obj };
}, {} as T);
}
}
Usage:
import { DataTransform } from './utils/data-transform';
const products = [
{ id: 1, category: 'electronics', price: 100 },
{ id: 2, category: 'electronics', price: 200 },
{ id: 3, category: 'books', price: 50 }
];
// Group by category
const grouped = DataTransform.groupBy(products, 'category');
// { electronics: [...], books: [...] }
// Sort by price
const sorted = DataTransform.sortBy(products, 'price', 'desc');
// Filter
const electronics = DataTransform.filterBy(products, { category: 'electronics' });
// Paginate
const page = DataTransform.paginate(products, 1, 2);
// { data: [...], total: 3, page: 1, pages: 2 }
// Stats
const prices = products.map(p => p.price);
const stats = DataTransform.stats(prices);
// { min: 50, max: 200, avg: 116.67, sum: 350, count: 3 }
Advanced Checklist
- • Historian used for time-series data
- • Appropriate aggregation selected
- • Time ranges optimized for performance
- • Function parameters validated
- • Workflow inputs structured correctly
- • Error handling implemented
- • Results cached where appropriate
- • Service layer used for advanced features
Resources
- •MACHHUB SDK Docs: https://docs.machhub.dev
- •Initialization Guide: See
machhub-sdk-initialization - •Real-time Data: See
machhub-sdk-realtime