FactoryFour Learn Center Index

Last updated March 24, 2019


Creating and Managing Workflow Deviations

Create custom workflows to manage deviations in response to quality forms when production errors occur.


Description:

Quality forms and task statuses allow operators to record errors during production. Use this combination of a task_form rule and a entity_status rule to create deviation workflows that will automatically resolve tasks in the error status.

The task_form rule creates a custom or template workflow in response to a value in a form submission. The entity_status rule listens for when deviation workflows complete, and updates the task blocked task from error to inactive if it hasn’t been resolved manually.

How to Use:

  1. Create a quality form for capturing production errors.
  2. Designate a field as a key metric using the ‘+ Add Key Metric’ button in the Form Builder. Single select dropdowns work best for error codes.
  3. For custom deviation workflows, your account will need to have a workflow named Custom Rework with the key custom_rework If you don’t already have a Custom Rework order workflow template, create one and create a revision to use as the default workflow.
  4. Create a new custom task_form Rule. Set the scope of the rule to the form you just created.
  5. Add the axios npm module. This module is required for the snippet to make calls to the FactoryFour API.
  6. The task form snippet below includes a sample configuration object which must be updated to your account. Copy this to your rule and save it.
  7. Modify the TARGET_METRIC value at the top of the code snippet to the key metric code for the error
  8. Modify the CONFIGURATION object using the following instructions
    /*
     * The rule checks this object for what operations to perform when the task form is submitted 
     * Specify actions for all the potential values your error code metric may have
     */
    const CONFIGURATION = {
     // Specify the value of the metric here
     'Field Value 1': {
         tags: {
             // Add tags to the order and task
             order: [{
                 key: 'Error', // Tag Group name
                 values: ['Error 1'] // Tag values, must be an array even if single select
             }],
             task: [{
                 key: 'Error', // Tag Group name
                 values: ['Error 1'] // Tag values, must be an array even if single select
             }],
         },
         workflows: ['template_workflow'], // workflow codes to add
         reworkTarget: ['task1'], // rework target for serial workflows
         skip: ['task2'], // tasks to skip in rework calculation
     },
     // Specify the value of the metric here
     'Field Value 1': {
         tags: {
             order: [{
                 key: 'Error', // Tag Group name
                 values: ['Error 1'] // Tag values, must be an array even if single select
             }],
             task: [{
                 key: 'Error', // Tag Group name
                 values: ['Error 1'] // Tag values, must be an array even if single select
             }],
         },
         workflows: ['template_workflow'], // workflow codes to add
         reworkTarget: ['task1'], // rework target for serial workflows
         skip: ['task2'], // tasks to skip in rework calculation
     }
    };
    
  9. Create a new custom entity_status rule. Set the scope of the rule for type: workflow, format: order, Order: Custom Rework, From Status: Any, To Status: Complete.
  10. Add the axios npm module. This module is required for the snippet to make calls to the FactoryFour API.
  11. The entity status snippet below will automatically resolve the task that initiated the deviation workflow. Copy this to your rule and save it.

Task Form Snippet

// Task Form Rule that creates a deviation workflow

const axios = require('axios');

const URL_BASE = 'https://api.factoryfour.com';

const TARGET_METRIC = 'error_code'; // key metric code to target

// Configuration object to allow for
const CONFIGURATION = {
	// key here is the field value
	'Field Value': {
		tags: {
			order: [{
				key: 'Error', // Tag Group name
				values: ['Error 1'] // Tag values, must be an array even if single select
			}],
			task: [{
				key: 'Error', // Tag Group name
				values: ['Error 1'] // Tag values, must be an array even if single select
			}],
		},
		workflows: ['template_workflow'], // workflow codes to add
		reworkTarget: ['task1'], // rework target for serial workflows
	}
};

/*
 * Retrieve a valid FactoryFour token from the headers in the context
 */
const getToken = (ctx) => {
	const token = ctx.headers['x-factoryfour-token'];
	if (!token) {
		throw new Error('Unable to find token in context');
	}
	return token;
};

/*
 * Retrieve the designated error code from the metrics calculated from the submitted form.
 */
const getErrorCode = (ctx) => {
	const errorMetric = ctx.body.formData.metrics
		.filter((m) => (m.code === TARGET_METRIC))[0];
	if (!errorMetric) {
		throw new Error('Could not find a metric with the matching code');
	}
	return errorMetric.value;
};

/*
 * Fetch the order this form was submitted on from the context
 */
const getOrderId = (ctx) => {
	const orderId = ctx.body.orders[0].id;
	if (!orderId) {
		throw new Error('Could not find an order for this form');
	}
	return orderId;
};

/*
 * Make calls to the FactoryFour API using the token passed to this rule and the workflowCode specified
 */
const addAndInitWorkflow = (data, token) => {
	// Format the headers for the request
	const headers = {
		Authorization: `Bearer ${token}`,
	};

	// Create a new workflow object
	return axios({
		method: 'POST',
		url: `${URL_BASE}/workflows/`,
		headers,
		data
	})
		.then((res) => {
			const workflowId = res.data.data.workflow.id;

			// Initialize the newly created workflow to create tasks
			const optionsInit = {
				method: 'POST',
				url: `${URL_BASE}/workflows/${workflowId}/initiate`,
				headers,
				json: true,
			};
			return axios(optionsInit);
		});
};

/*
*  Search for a Workflow ID in the context
* */
const getWorkflowInfo = (ctx) => {
	const workflowId = ctx.body.tasks[0].workflowParent;
	const taskCode = ctx.body.tasks[0].code;
	const taskId = ctx.body.tasks[0].id;
	if (!workflowId) {
		throw new Error('Could not find an workflow for this form');
	}
	return {
		workflowId,
		taskCode,
		taskId
	};
};

/*
 * Retrieve a workflow tasks from the workflows service
 */
const retrieveWorkflowTasks = (wofklowId, token) => {
	// Format the headers for the request
	const headers = {
		Authorization: `Bearer ${token}`,
	};

	// Create a new workflow object
	return axios({
		method: 'GET',
		url: `${URL_BASE}/workflows/${wofklowId}?fetchTasks=true`,
		headers,
	})
		.then((res) => res.data.data.tasks.map(t => ({
			key: t.code === 'end' ? 'end' : t.id,
			code: t.code,
			dependsOn: t.dependsOn,
		})));
};

/*
* Calculate a path from target to current in the workflow
* */
const calculateReworkflow = (target, current, tasks, skip = []) => {
	const targetNode = tasks.filter(task => (task.code === target))[0];
	const currentNode = tasks.filter(task => (task.code === current))[0];
	if (!targetNode) {
		throw new Error ('Could not find target node');
	}
	if (!currentNode) {
		throw new Error ('Could not find current node');
	}

	targetNode.dependsOn = ['start'];

	const newWorkflow = [targetNode];

	let pointer = targetNode;
	const filter = task => (task.dependsOn.includes(pointer.key));
	while (!currentNode.dependsOn.includes(pointer.key)) {
		const dependant = tasks.filter(filter)[0];
		if (!dependant) {
			throw new Error('Could not find dependant on node');
		}
		if (dependant.code === 'end') {
			throw new Error('Could not find current node in dependants of target');
		}
		const toPush = Object.assign({}, dependant);
        toPush.dependsOn = [newWorkflow[newWorkflow.length - 1].key];
        if (!skip.includes(toPush.code)) {
            newWorkflow.push(toPush);
        }
		pointer = dependant;
	}
	newWorkflow.push({
		key: 'end',
		code: 'end',
		dependsOn: [newWorkflow[newWorkflow.length - 1].key],
	});

	return newWorkflow;
};

module.exports = (context, cb) => {
	const token = getToken(context);
	const errorCode = getErrorCode(context);
	const orderId = getOrderId(context);
	const { workflowId, taskCode, taskId } = getWorkflowInfo(context);

	// Check if a configuration exists and log which once we're using
	const config = CONFIGURATION[errorCode];
	if (!config) {
		console.log(`No configuration was found for value "$${errorCode}".`);
		return cb(null);
	}
	console.log(JSON.stringify({
		message: 'Using the following configuration',
		errorCode,
		config,
	}, null, 4));

	const workflowCreationPromises = [];

	// Search the map for a workflow to add
	const workflowsToAdd = config.workflows;
	if (workflowsToAdd) {
	// if no workflow was found, exit with no further action
		if (!Array.isArray(workflowsToAdd)) {
			console.log('Workflow creation requires an array');
			return cb('Workflow creation requires an array');
		}
		workflowsToAdd.forEach((wf) => {
			workflowCreationPromises.push(addAndInitWorkflow({
				format: 'order',
				parent: `f4::order::${orderId}`,
				code: wf
			}, token));
		});
	}

	// Run workflow promises
	return retrieveWorkflowTasks(workflowId, token)
		.then((workflowTasks) => {
			console.log(JSON.stringify(workflowTasks, null, 4));

			const reworkflowsToAdd = config.reworkTarget;
			if (reworkflowsToAdd) {
				if (!Array.isArray(reworkflowsToAdd)) {
					console.log('Reworkflow creation requires an array');
					return cb('Reworkflow creation requires an array');
				}
				reworkflowsToAdd.forEach((rwfTarget) => {
					const taskTemplate = calculateReworkflow(rwfTarget, taskCode, workflowTasks);
					workflowCreationPromises.push(addAndInitWorkflow({
						format: 'order',
						parent: `f4::order::${orderId}`,
						code: 'custom_rework',
						name: 'Custom Rework',
						metadata: {
							deviationInitiator: `f4::task::${taskId}`,
						},
						taskTemplate,
					}, token));
				});
			}

			return Promise.all(workflowCreationPromises);
		})
		.then(() => {
			if (!config.tags) {
				return cb(null, {});
			}
			
			// Return the tags that should be added once workflow creation steps are complete
			return cb(null, { tag: {
				task: { add: config.tags.task },
				order: { add: config.tags.order },
			} });
		})
		.catch((err) => {
			console.log(err);
			return cb('Could not deviate successfully');
		});
};

Entity Status Snippet

// Entity status rule that listens for deviation workflows to complete

const axios = require('axios');

const URL_BASE = 'https://api.factoryfour.com';

/*
 * Retrieve a valid FactoryFour token from the headers in the context
 */
const getToken = (ctx) => {
	const token = ctx.headers['x-factoryfour-token'];
	if (!token) {
		throw new Error('Unable to find token in context');
	}
	return token;
};

/*
 * Determine the rework target if the workflow was initiated due to rework
 */
const getWorkflowReworkTarget = (context) => {
    const { entity } = context.body;
    if (!entity || !entity.type || entity.type !== 'workflow') {
        return null;
    }
    if (!entity.metadata || !entity.metadata.deviationInitiator) {
        return null;
    }
    const spl = entity.metadata.deviationInitiator.split('::');
    if (spl.length < 3 || spl[0] !== 'f4' || spl[1] !== 'task') {
        return null;
    }
    return spl[2];
};

const reinitializeTask = (taskTarget, token) => {
	// Format the headers for the request
	const headers = {
		Authorization: `Bearer ${token}`,
	};

	// Create a new workflow object
	return axios({
		method: 'GET',
		url: `${URL_BASE}/tasks/${taskTarget}?fetchOptions=true`,
		headers,
	})
		.then((res) => {
            const { task } = res.data.data;
            if (task.status !== 'error' || !task.options.includes('inactive')) {
                return false;
            }
            if (task.optionsEnforcement && Array.isArray(task.optionsEnforcement) && task.optionsEnforcement.length > 0) {
                return false;
            }

			// Initialize the newly created workflow to create tasks
			const optionsInit = {
				method: 'PUT',
				url: `${URL_BASE}/tasks/${taskTarget}/status`,
				headers,
				json: true,
                data: {
                    status: 'inactive'
                }
			};
			return axios(optionsInit);
		});
};

module.exports = function (context, cb) {
	const token = getToken(context);
	const taskTarget = getWorkflowReworkTarget(context);

    if (!taskTarget) {
        return cb(null, {});
    }
    return reinitializeTask(taskTarget, token)
        .then(() => {
            return cb(null, {});
        })
        .catch((err) => {
            console.log(err);
            return cb(err);
        });
};

Search the FactoryFour Learn Center


FactoryFour Learn Center