API

Information about the ion GraphQL API and example queries

Developers: How to create an API Key:

pageAPI Keys

Getting started

To get started with the ION API, we recommend learning about GraphQL and playing around with the interactive API explorer in the ION web application.

Once you've played around a bit, you can request an API key and access token through this self-service process.

Example queries

Change Run Step status

Find the id of the runStep you want to change, replacing the run id below with the run ID you are changing.

{
  run(id: 372) {
    steps {id status _etag}
  }
}

You will see a response like the following. Notice the run step that is failed. That's the one we want to change in the subsequent mutation

{
  "data": {
    "run": {
      "steps": [
        {
          "id": 1133,
          "status": "todo",
          "_etag": "b0fe83a65319455685e760614f585099"
        },
        {
          "id": 1134,
          "status": "todo",
          "_etag": "9e1aa0e8909345cdb63e25289b7ce916"
        },
        {
          "id": 1135,
          "status": "failed",
          "_etag": "e3bb9a721745409cb4c20ca3aa1d591a"
        }
      ]
    }
  }
}

Once you've found the id and _etag for the runStep you want to move back to "todo", use the following mutation to affect the change. You will also need to define the variables in the "Query Variables" section below the query explorer.

mutation UpdateRunStep($input: UpdateRunStepInput!) {
  updateRunStep(input: $input) {
    runStep {
        id status
    }
  }
}

Query variables:

{
  "input": {
    "id": 1135,
    "status": "todo",
    "etag": "e3bb9a721745409cb4c20ca3aa1d591a"
  }
}

After you submit the query, you will get a response containing the run step's ID and the newly set status.

Redlines

Redlines are used to demarcate any deviation a particular run has from the procedure that created the run. Redlining a step allows an engineer to edit a step or its fields. Redlining also allows for the creation of additional steps and modifying the dependency graph of the steps downstream from the step being redlined. This powerful feature allows an engineer to alter branches of a run, while not affecting other branches which are running concurrent to the step or steps being redlined. To show how redlines work in ION we will imagine a run with the execution graph below.

Creating a Redline

A redline is created by updating a step to the status of redline. You must have the "engineer" role to do this.

mutation UpdateRunStep($input: UpdateRunStepInput!) {
    updateRunStep(input: $input) {
        runStep {
            id status title _etag content redlines { diff id }
        }
    }
}

Query variables:

{
    "input": {
        "id": 4,
        "status": "redline",
        "etag": "etag"
    }
}

Now the run execution graph has entered a redline state, steps 2 and 3 are still able to be executed since they are on a different branch than the redlined step. All steps downstream from the step in redline cannot be executed and have entered a special dag_modifiable (previously, content_locked) state which will be described in further detail below.

Editing a Step in Redline

Now that step 4 is in redline, we can edit the step by altering its content, title, or fields. Using the above defined UpateRunStep mutation with the below variables we edit the title of the step.

Query variables:

{
    "input": {
        "id": 4,
        "title": "redline",
        "etag": "new_etag"
    }
}

Adding a new Step (Redlined step)

We can also create a new step within the run. Below we will discuss how to add this new step to run execution graph. The new step will be created with the status of redline, so that it can be altered after creation.

mutation CreateRunStep($input: CreateRunStepInput!) {
    createRunStep(input: $input) {
        step {
            id title content _etag
        }
    }
}

Query variables:

{
    "input": {
        "title": "step created in redline",
        "content": "new step content",
        "runId": 1
    }
}

The run execution graph would now look like the diagram below.

Editing the Graph

When a step is put in redline, all of its downstream steps which have the status of todo or in_progress are put into a special status known as content_locked. This dag_modifiable status prevents a user from altering any of the content within a run step, but does allow the user to edit its graph edges. We will use the below mutations to add the newly created step into our run execution graph.

mutation CreateRunStepEdge($input: CreateStepEdgeInput!) {
    createRunStepEdge(input: $input) {
        stepEdge {
            id stepId upstreamStepId  
        }
    }
}

Query variables:

{
    "input": {
        "stepId": 8,
        "upstreamStepId": 6
    }
}
{
    "input": {
        "stepId": 7,
        "upstreamStepId": 8
    }
}

After creating the new edges, we will remove the existing edge between steps 6 and 7.

mutation DeleteRunStepDAG($id: ID!, $etag: String!) {
    deleteRunStepEdge(id: $id, etag: $etag) {
        id
    }
}

Query variables:

{
    "id": 6,
    "etag": "edge_etag"
}

Now step 8 is within the run execution graph and the current state is reflected below.

Finishing a Redline

A redline is completed by updating the status of a run step being redlined to todo. Completing the redline will allow the user to see a diff of what was altered during the redline. Ending a redline also converts all downstream steps which have the status dag_modifiableback into the status of todo. Using the UpdateRunStep mutation defined above with the below query variables will finish a redline.

{
    "input": {
        "id": 4,
        "status": "todo",
        "etag": "current_step_etag"
    }
}

Steps 4, 5, and 6 have moved back to the status of todo.

Because there are two steps in redline, we also have to update step 8 to a status of todo so that the run no longer has any steps in redline.

{
    "input": {
        "id": 8,
        "status": "todo",
        "etag": "current_step_etag"
    }
}

Canceling a Redline

If no edges were added or removed within a redline, then that redline can be canceled and all changes will be reverted to their original state. If the execution graph edges have been altered than the user must revert the edges to their original state before a redline can be canceled. Below is the mutations to cancel a redline.

mutation CancelRunStepRedline($id: ID!, $etag: String!) {
    cancelRunStepRedline(id: $id, etag: $etag) {
        runStep { id title status content }
    }
}

Query variables (id and etag of the run step)

{
    "id": 4, 
    "etag": "new_etag" 
}

File Attachments and Assets

The ION API differentiates between files into two different types: assets and file attachments. The driving force behind this separation is to provide greater access control and validation to file handling. For instance when a procedure is instantiated as a run, all the procedure steps' assets that were created by an engineer are copied over to that run, and any files attached by an operator during the run execution are saved as file attachments. Another way to think about the difference is that assets are defined while file attachments are collected.

Getting a File

Getting a file attachment or asset can be done with the same query which returns the filename and other meta data about the file. It does not return the file itself, but along with the metadata it returns a signed URL which provides temporary and secure access to the file within the ION file storage system.

query GetFile {
    fileAttachment(id: 1) {
        id url s3Bucket s3Key filename
    }
}

Assets

All asset mutations require the user to have the "engineer" role. The two places assets can be added and deleted are on procedure steps and run steps when the run step is in redline. To create an asset you can make a request with the entityId of the object to attach the asset to. Below is an example of creating an asset for a step with entity id 1. The response will contain a secure link to upload your file to using the HTTP PUT method.

Create Asset

mutation CreateAsset($input: CreateFileAttachmentInput!) {
    createAsset(input: $input) {
        fileAttachment { id s3Key entityId }
    }
}
{
    "entityId": 1 
}

Delete Asset

Creating an asset creates a file object with an ID. To delete an asset, use the below mutation with the new file object id and the steps entity id.

mutation DeleteAsset($input: DeleteFileAttachmentInput!) {
    deleteAsset(input: $input) {
        fileAttachmentId entityId
    }
}
{
    "fileAttachmentId": 2, 
    "entityId": 1 
}

File Attachments

File attachments are used to attach files to a run or run step during a run's execution, as well as to parts.

Create File Attachment

Uploading files directly using our API has been deprecated and will be removed in the future. The preferred way is to use a signed s3 URL which allows a user to upload a file directly to s3. This is done be creating a file attachment in our system and then using the returned URL to post the file to s3.

import requests
from urllib.parse import urljoin
import os
from dataclasses import dataclass


CREATE_FILE_ATTACHMENT = """
mutation CreateFileAttachment($input: CreateFileAttachmentInput!) {
    createFileAttachment(input: $input) {
        fileAttachment {
            id
            entityId
            filename
            contentType
            s3Bucket
        }
        uploadUrl
    }
}
"""

ION_API_URI = os.getenv("ION_API_URI")
HEADERS = {
    "Authorization": f"Bearer {os.getenv('ION_API_TOKEN')}",
    "Content-Type": "application/json",
}


@dataclass
class FileAttachmentInfo:
    """File Attachment dataclass."""

    name: str
    id: int
    upload_url: str
    content_type: str


def create_ion_file_attachment(
    file_name: str,
) -> FileAttachmentInfo:
    """Create file attachment object in ion."""
    resp = make_ion_request(
        request_json={
            "query": CREATE_FILE_ATTACHMENT,
            "variables": {
                "input": {
                    "filename": file_name,
                    "entityId": entity_Id,
                }
            },
        },
    )
    file_info_dict = resp["data"]["createFileAttachment"]
    return FileAttachmentInfo(
        id=file_info_dict["fileAttachment"]["id"],
        name=file_info_dict["fileAttachment"]["filename"],
        content_type=file_info_dict["fileAttachment"]["contentType"],
        upload_url=file_info_dict["uploadUrl"],
    )


def make_ion_request(request_json: dict) -> dict:
    """Make a request to ion graphql api and return response data."""
    with requests.post(
        urljoin(ION_API_URI, "graphql"),
        json=request_json,
        headers=HEADERS,
    ) as response:
        response.raise_for_status()
        response_json = response.json()
        return response_json


def upload_file_to_s3(file_attachment: FileAttachmentInfo, file_object: bytes):
    """Upload file to s3 with signed URL."""
    with requests.put(
        url=file_attachment.upload_url,
        headers={"Content-Type": file_attachment.content_type},
        data=file_object,
    ) as response:
        response.raise_for_status()


if __name__ == "__main__":
    file_name = 'swag.txt'
    file_attachment = create_ion_file_attachment(file_name)
    with open(file_name, 'rb') as f:
        upload_file_to_s3(file_attachment, f.read())

Deprecated:

mutation CreateFileAttachment($input: CreateFileAttachmentInput!) {
    createFileAttachment(input: $input) {
        fileAttachment { id s3Key entityId }
    }
}
{
    "entityId": 2 
}

Delete File Attachment

mutation DeleteFileAttachment($input: DeleteFileAttachmentInput!) {
    deleteFileAttachment(input: $input) {
        fileAttachmentId entityId
    }
}
{
    "fileAttachmentId": 3, 
    "entityId": 2 
}

Unattached Files

There are some situations, such as for the thumbnail of a part or a file for a run step field which require a file to be uploaded without attaching it to an object. To do this use the same create file attachment mutation above, but do not include an entity id. As is the case with other file uploads, you will receive a secure link to upload the file to using the HTTP PUT method.

Reviews Requests and Reviews

Before a procedure is released it must go through a review process. The number of reviewers required to release a procedure can be set in an organization's settings. A procedure is put into review by changing its status to in_review via an update procedure mutation.

mutation UpdateProcedure($input: UpdateProcedureInput!) {
    updateProcedure(input: $input) {
        procedure {
            id title description status createdById
        }
    }
}
{
    "id": 4, 
    "status": "in_review",
    "etag": "etag" 
}

Once a procedure is in review it cannot be edited. Review requests can be sent out with the following mutation. Each review request will also send out notification to the user whom you requested a review from.

mutation CreateReviewRequest($input: ReviewRequestInput!) {
    createReviewRequest(input: $input) {
        reviewRequest { id reviewer { id } status entityId }
    }
}
{
    "procedureId": 4, 
    "reviewerId": 1
}

Once a review has been requested that user can leave a review of pending, approved or rejected. To leave a review that is just a comment, use the status pending as it will neither reject nor approve the procedure.

mutation CreateReview($input: CreateReviewInput!) {
    createReview(input: $input) {
        review { id organizationId status entityId }
    }
}
{
    "procedureId": 4, 
    "status": "pending",
    "comment": "Just leaving a comment"
}

You cannot delete reviews, but if someone should not review a procedure you can delete their review request using the following mutation.

mutation DeleteReviewRequest($id: ID!, $etag: String!) {
    deleteReviewRequest(id: $id, etag: $etag) { id }
}
{
    "id": 1, 
    "etag": "etag"
}

Once all requested reviewers have approved, and the minimum number of reviewers required has been met than a procedure is automatically released.

You can list all the existing review requests with a few filters, the query below lists all requested reviews for a specific procedure.

query GetReviewRequests($filters: ReviewsInputFilters) {
    reviewRequests(filters: $filters) {
        edges {
            node { id procedureId reviewerId status}
        }
    }
}
{
    "procedureId": {
        "eq": 4
    }
}

Along with listing review requests, you can also list all the reviews.

query GetReviews($filters: ReviewsInputFilters) {
    reviews(filters: $filters) {
        edges {
            node { id procedureId reviewerId comment status}
        }
    }
}
{
    "procedureId": {
        "eq": 4
    }
}

Issues

ION's issue system is rather flexible and has several controls within an organization's settings. There are settings to automatically create issues when a step fails, to prevent a step from being moved back to todo while open issues exist, and to set the number of approvals required till an issue can be marked as resolved. Below is an diagram that shows the non-conformance workflow of issues and each disposition type.

You can use a series of filters to list current issues by status, disposition type, and a few other filters. The query below lists all issues related to a specific run step.

query GetIssues($filters: IssueInputFilters, $sort: [IssueSortEnum]) {
    issues(sort: $sort, filters: $filters) {
        edges{node {
            id dispositionType causeCondition expectedCondition
            _etag title status disposition
            runStep { id status title }
        }}
    }
}
{
    "runStepId": {
        "eq": 1
    }
}

To create an issue, you can use the following mutation. An issue is required to be linked to a run step.

mutation CreateIssue($input: CreateIssueInput!) {
    createIssue(input: $input) {
        issue {
            id dispositionType causeCondition 
            expectedCondition disposition status
            _etag title runStep { id status title }
        }
    }
}
{
    "runStepId": 4, 
    "dispositionType": "repair",
    "title": "Part instance needs fixing"
}

Once an issue is created you can update it's values using the following mutation.

mutation UpdateIssue($input: UpdateIssueInput!) {
    updateIssue(input: $input) {
        issue {
            id dispositionType causeCondition expectedCondition
            disposition status _etag title
            runStep { id status title }
        }
    }
}
{
    "disposition": "Rewire circuit",
    "causeCondition": "5V across the resistor",
    "expectedCondition": "3V across the resistor"
}

When an issue is created, its status defaults to "pending". An issue can have its status moved to in_progress, in_review or resolved using the above update mutation. An issue has an approval process very similar to that of procedures. And an issue cannot be moved into resolved until all the requested reviews have been approved and the minimum number of approvals has been met. Lastly an issue cannot be resolved without setting its disposition type.

Errors

Any errors happening on the API will be returned in the errors array following the GraphQL convention.

Known errors

Known errors are extended with an error code and their payload will look as follows :

{ message: 'Validation error', extensions: { code: 'VALIDATION_ERROR'}}

Clients using the API can use the error code to respond accordingly. We currently support the following error codes:

  • VALIDATION_ERROR - Request to server is not valid (e.g. out of range numbers, parameters omitted, etc)

  • AUTHORIZATION_ERROR - API caller is not authorized to perform action

  • NOT_FOUND_ERROR - Resource is not found in the database

  • EXTERNAL_SERVICE_ERROR - An external service is not responding as expected (e.g. email service)

  • CONCURRENCY_ERROR - Multiple clients modifying the same resource against outdated data is prevented in the API

Unexpected errors

Unexpected errors might not have an extensions attribute and therefore might no include an error code. Client's using the API should guard against this and handle unexpected errors accordingly.

Last updated