JavaScript Functions, Standardized
Every JavaScript developer knows the friction of consuming an unfamiliar npm package. You read the docs, guess at the API, and hope the function signatures match your expectations. The machine specification, authored by Mike McNeil, aims to remove that guesswork by defining a universal, predictable interface for JavaScript functions.
Machines are self-documenting functions built to this specification. Each machine has one clear purpose — sending an email, issuing a JSON Web Token, making a fetch request — and its structure makes both its behavior and its inputs obvious at a glance. This predictability extends to debugging: because every machine follows the same contract, tracing issues becomes straightforward.
Machines are already in wide circulation. If you've used the Sails.js MVC framework, you've interacted with them (McNeil authored both Sails.js and the machine spec). You can browse published machinepacks on npm by searching for the machinepack prefix, or visit the registry at node-machine.org/machinepacks, which syncs with npm and updates every ten minutes.
The Anatomy of a Machine
At its core, a machine is an exported object with standardized properties and a single function. Here's a representative example from the official spec:
module.exports = {
friendlyName: 'Do something',
description: 'Do something with the provided inputs that results in one of the exit scenarios.',
extendedDescription: 'This optional extended description can be used to communicate caveats, technical notes, or any other sort of additional information which might be helpful for users of this machine.',
moreInfoUrl: 'https://stripe.com/docs/api#list_cards',
sideEffects: 'cacheable',
sync: true,
inputs: {
brand: {
friendlyName: 'Some input',
description: 'The brand of gummy worms.',
extendedDescription: 'The provided value will be matched against all known gummy worm brands. The match is case-insensitive, and tolerant of typos within Levenstein edit distance <= 2 (if ambiguous, prefers whichever brand comes first alphabetically).',
moreInfoUrl: 'https://gummy-worms.org/common-brands?countries=all',
required: true,
example: 'haribo',
whereToGet: {
url: 'https://gummy-worms.org/how-to-check-your-brand',
description: 'Look at the giant branding on the front of the package. Copy and paste with your brain.',
extendedDescription: 'If you don\'t have a package of gummy worms handy, this probably isn\'t the machine for you. Check out the `order()` machine in this pack.'
}
}
},
exits: {
success: {
outputFriendlyName: 'Protein (g)',
outputDescription: 'The grams of gelatin-based protein in a 1kg serving.',
},
unrecognizedFlavors: {
description: 'Could not recognize one or more of the provided `flavorStrings`.',
extendedDescription: 'Some **markdown**.',
moreInfoUrl: 'https://gummyworms.com/flavors',
}
},
fn: function(inputs, exits) {
// ...
// your code here
var result = 'foo';
// ...
// ...and when you're done:
return exits.success(result);
};
}
The top-level properties describe the machine itself. friendlyName is a display name in sentence-case, under 50 characters, with no ending punctuation. description is a one-sentence summary in the imperative mood ("Issue a JSON Web Token," not "Issues a JSON Web Token"), under 80 characters. Two optional fields provide depth: extendedDescription (under 2000 characters, full sentences allowed) and moreInfoUrl, which is useful for machines that wrap third-party APIs.
Two optional flags affect behavior. sideEffects can be set to cacheable or idempotent, and only machines without side effects should be marked cacheable — which enables the .cache() method. By default, machines are asynchronous; setting sync to true makes the machine a regular synchronous function.
Inputs
The inputs object declares what values the machine's function expects. Each input key is camel-cased, starts with a lowercase letter, and contains no special characters. Every input has its own friendlyName, description, and optional extendedDescription and moreInfoUrl. The example field defines the expected data type. Inputs are optional by default — if a value isn't provided, the fn receives undefined — so set required: true to make the machine throw an error for a missing value. For sensitive or hard-to-find inputs (API keys, tokens), the whereToGet object offers imperative-mood instructions on locating suitable values.
Exits
Exits define every possible outcome of the machine's function. The success exit is standardized and includes outputFriendlyName and outputDescription to describe the return value. Error exits follow the naming conventions of input keys. Each error exit has a description of when it triggers and can optionally include an extendedDescription with full Markdown support.
There's a learning curve here, but the conventions become second nature after authoring a single machine.
Machinepacks: What You Publish
On npm, you publish machinepacks — bundles of related machines for common development tasks. A machinepack working with arrays might include machines for concat(), map(), and similar operations. The naming convention is strict: every machinepack name must carry the machinepack- prefix followed by the descriptive name, like machinepack-array.
You can explore the Arrays machinepack in the registry to see a complete, working example.
Building a Machinepack
To illustrate authoring, we'll create a wrapper machinepack around the file-contributors npm package. Three tools get us started:
- Machinepack CLI: Install with
npm install -g machinepacknpm install -g machinepack - Yeoman scaffolding tool: Install globally with
npm install -g yo - Machinepack Yeoman generator: Install with
npm install -g generator-machinepack
Node.js and npm should already be installed. Note that the Yeoman generator has reported issues with Node.js 12 and 13 — for this workflow, use nvm to run Node.js 10.x, which is a confirmed working environment.
Generation and Exploration
Navigate to your target directory and run the generator:
yo machinepack
The interactive prompt scaffolds a barebones machinepack — answer yes to creating the example machine. The result produces these files:
DELETE_THIS_FILE.md
machines/
package.json
package.lock.json
README.md
index.js
node_modules/
With the Machinepack CLI installed, you can inspect and interact with any machinepack locally. To list available machines in the machines directory, run:
machinepack ls
This reveals the say-hello machine that ships with the generator. To see it in action, run:
machinepack exec say-hello
The CLI prompts for a name and prints the machine's output. Notice how the tool leverages standardization to derive description and functionality directly from the machine's metadata — no special handling needed.
Building Your Own Machine
To create a custom machine that wraps the file-contributors and node-fetch packages, first install those dependencies:
npm install file-contributors node-fetch --save
Next, generate a new machine using the CLI:
machinepack add
The CLI will prompt you for a friendly name, an optional description, and an optional extended description. Once you finish, the machine files are generated for you.
Open the generated machine in your editor and require the file-contributors package:
const fetch = require('node-fetch');
const getFileContributors = require('file-contributors').default;
global.fetch = fetch; // workaround since file-contributors uses windows.fetch() internally
Note: The node-fetch package and the global.fetch = fetch workaround are necessary because file-contributors calls windows.fetch() internally, which is unavailable in Node.js.
The getFileContributors function requires three arguments: owner (repository owner), repo (repository name), and path (file path). These belong in the inputs key:
...
inputs: {
owner: {
friendlyName: 'Owner',
description: 'The owner of the repository',
required: true,
example: 'DominusKelvin'
},
repo: {
friendlyName: 'Repository',
description: 'The Github repository',
required: true,
example: 'machinepack-filecontributors'
},
path: {
friendlyName: 'Path',
description: 'The relative path to the file',
required: true,
example: 'README.md'
}
},
...
For exits, the CLI creates a success exit by default. Modify that and add an error exit for failure cases:
exits: {
success: {
outputFriendlyName: 'File Contributors',
outputDescription: 'An array of the contributors on a particular file',
variableName: 'fileContributors',
description: 'Done.',
},
error: {
description: 'An error occurred trying to get file contributors'
}
},
Finally, implement the core logic in the fn function:
fn: function(inputs, exits) {
const contributors = getFileContributors(inputs.owner, inputs.repo, inputs.path)
.then(contributors => {
return exits.success(contributors)
}).catch((error) => {
return exits.error(error)
})
},
You've built your first machine. Test it with the CLI:
machinepack exec get-file-contributors
You'll be prompted for owner, repo, and path in sequence. If everything works, the machine exits with success and returns an array of contributors for the specified file.
Consuming Machines in Code
In a real application you won't call machines through the CLI. Here's how to consume a machine from a machinepack programmatically:
var FileContributors = require('machinepack-filecontributors');
// Fetch metadata about a repository on GitHub.
FileContributors.getFileContributors({
owner: 'DominusKelvin',
repo: 'vue-cli-plugin-chakra-ui',
path: 'README.md'
}).exec({
// An unexpected error occurred.
error: function (){
},
// OK.
success: function (contributors){
console.log('Got:\n', contributors);
},
});
Wrapping Up
You now know the machine specification, have built a machine of your own, and understand how to invoke machines from code.
Resources
- "Getting Started", node machine
- file contributors, npm
The source repository and the published npm package are available for reference.




