Extra Horizon
GitHub
  • Extra Horizon Documentation
  • Getting Started
    • Start familiarizing yourself
  • Tutorials
    • Medical Device Tutorial
      • Preparation
      • Build your first prototype
        • Define a data model
        • Configure your workflows
          • Workflow 1: Analyze a measurement
          • Workflow 2: Create & store a PDF report
          • Workflow 3: Send an e-mail
        • Define your permissions
          • Update your schema with user permissions
          • Update your schema with group permissions
        • Build the Front-End
          • Set up oAuth in your backend
          • Demo login page
      • Summary & Wrap up
    • Polysomnography (PSG) Tutorial
    • Retool - Building dashboards Tutorial
  • FAQ
    • General
  • Services
    • Identity and Access Management
      • User service
        • Users
        • Groups
        • Global roles
        • Configuration
      • Auth Service
        • Applications
        • OAuth2
        • OAuth1
        • MFA
        • OpenID Connect
          • Google Cloud
          • Azure ADFS
    • Data Management
      • File Service
      • Data Service
        • Schemas
        • Documents
        • FAQ Data Service
    • Automation
      • Task Service
        • Functions
        • Tasks
        • API Functions
        • Examples
          • Hello world (JS)
          • Hello world (Py)
          • Hello world (Docker)
        • FAQ
      • Dispatchers Service
      • Event Service
        • System Events
    • Communication
      • Notification Service
        • Notifications
        • Settings
      • Mail Service
    • Other
      • Localization Service
        • Language Codes
      • Template Service
        • Localizations
      • Payments Service
        • Subscriptions
        • Stripe
        • iOS App Store
      • Configurations Service
  • API Reference
    • OpenAPI Specifications
    • 📦Changelog
      • Per-service Changelog
    • Postman Reference Collection
  • Tools
    • SDK
    • CLI
    • Control Center
  • Additional Resources
    • Resource Query Language (RQL)
    • Handling Errors
    • GitHub
    • API interaction (Python)
    • Migration guide: Enabling verification request limiting
  • ExH Platform
    • 🙋Support
    • ⏱️Usage and Performance
    • 🗺️Regions
    • ⚖️Cloud Subscription Agreement
    • Security, Privacy & Quality
      • 🔓Security
      • 🇺🇸CFR 21 Part 11
      • ExH as OTS Software
Powered by GitBook
On this page
  • Basic concepts
  • Create a PDF template
  • Update schema
  • Update your task to create a PDF
  • Deploy everything
  • Test the updated task
  • Test your task locally

Was this helpful?

  1. Tutorials
  2. Medical Device Tutorial
  3. Build your first prototype
  4. Configure your workflows

Workflow 2: Create & store a PDF report

PreviousWorkflow 1: Analyze a measurementNextWorkflow 3: Send an e-mail

Last updated 6 months ago

Was this helpful?

Now that the measurement is analyzed, let's create a simple PDF report which is stored in the file service & can be downloaded by your users.

Basic concepts

In the previous workflow, we've used the data service to save measurement data to Extra Horizon. The data service is designed for storing structured data. However, not all data can be written in a structured manner.

In this section, we'll leverage the file service to handle unstructured data. While the data-service is designed for storing structured data, the file service is ideal for managing unstructured data. In this section, you'll learn how to create a PDF document from a template and seamlessly interact with the file service using the SDK. You can learn more about the file service .

Every file stored in the file service receives a unique token. These tokens are essential for retrieving files from the file service. Within the function, we will add the token for the PDF to the blood pressure measurement. This way, users can effortlessly access their reports whenever needed.

Create a PDF template

Extra horizon has a separate service for managing templates. This is typically used for creating mails or PDF's, but it's not necessarily limited to that. Because they are managed by a service, they can be updated independently of the tasks that are using them. For more information about the template service, see .

Under 2-workflows/templates/pdf-analysis you'll find the template that we're going to use.

{
  "description": "Template used to generate a pdf-analysis",
  "name": "pdf-analysis",
    "schema": {
    "type": "object",
    "fields": {
      "first_name": {
        "type": "string"
      },
      "category": {
        "type": "string"
      },
      "diastolic": {
        "type": "number"
      },
      "systolic": {
        "type": "number"
      },
      "date": {
        "type": "string"
      }
    }
  },
  "fields": {
  }
}
<!DOCTYPE html>
<head>
    <style>
        table, th, td {
            border: 1px solid black;
            border-collapse: collapse;
        }
    </style>
</head>
<html>
<body>

<h1>Your reading from $content.date</h1>

<table style="width:100%">
    <tr>
        <th>Diastolic</th>
        <td>$content.diastolic</td>
    </tr>
    <tr>
        <th>Systolic</th>
        <td>$content.systolic</td>
    </tr>
    <tr>
        <th>Category</th>
        <td>$content.category</td>
    </tr>
</table>

This template consists of 2 files: an HTML file and a JSON file.

The JSON file defines the structure of the template variables, while the HTML file is the template where those variables are used.

The reason why it is split in 2 files, is because mixing HTML and JSON in 1 file doesn't make for a pleasant developer experience.

When uploading the template, the CLI will read the HTML file, add it as a body property in the fields object in the JSON file and upload the result.

Update schema

We are going to update our schema a bit to accomodate this new feature:

  • An additional property to store a file token.

  • A new status to indicate that the report is available, report-available

  • Since we added a new status, we also need a new transition. We only allow transitions to the report-availabe state coming from the analyzed state.

As mentioned in the introduction, a file token is something you get back when you store a new file in the file-service. It's your personal key to access the file. Make sure to store this token in a secure way, because any user with this token is able to download the file.

{
  "name": "blood-pressure-measurement",
  "description": "Blood pressure measurement",
  "statuses": {
    "created": {},
    "analyzing": {},
    "analyzed": {},
    "report-available": {}
  },
  "creationTransition": { ... },
  "transitions": [
    ...
    {
      "name": "add-report",
      "type": "manual",
      "toStatus": "report-available",
      "fromStatuses": [ "analyzed" ],
        "conditions": [
          {
            "type": "input",
            "configuration": {
              "type": "object",
              "properties": {
                "report": {
                  "type": "string"
                }
              },
              "required": ["report"]
            }
          }
        ]
    }
  ],
  "properties": {
    ...
    "report": {
      "type": "string",
      "description": "File-token of the report"
    }
  }
}

Update your task to create a PDF

Next we need to update the task to create the PDF, upload it to the file service and store the file token in the document.

const { getSDK } = require("./sdk");
const { analyzeDocument } = require("./diagnose");
const { createPDF } = require("./create-pdf");

exports.doTask = async ({sdk, task}) => {
  //Read the blood pressure document
  const retrievedDocument= await sdk.data.documents.findById('blood-pressure-measurement', task.data.documentId);

  /* Analyze the document */
  const diagnosis = await analyzeDocument({ sdk, document: retrievedDocument});

  //Fetch the info of the user who created this document
  const user = await sdk.users.findById(retrievedDocument.creatorId);

  //Create the PDF
  await createPDF({sdk, user, document: retrievedDocument, diagnosis });
}

exports.handler = async (task) => {
  const sdk = await getSDK();

  await exports.doTask({sdk, task});
};

async function createPDF({sdk, user, document, diagnosis}) {
     // Find the pdf template
    const pdfTemplate = await sdk.templates.findByName("pdf-analysis");

    // Generate the PDF with the template
    const pdf = await sdk.templates.resolveAsPdf(pdfTemplate.id, {
        "language": "NL",
        "time_zone": "Europe/Brussels",
        "content": {
            "first_name": user.first_name,
            "category": diagnosis,
            "diastolic": document.data.diastolic,
            "systolic": document.data.systolic,
            "date": new Date(document.data.timestamp).toDateString(),
        }
    });


    // Find the id of the transition, needed for transitioning the document
    const schema = await sdk.data.schemas.findByName('blood-pressure-measurement');
    const transition = schema.transitions.find(transition => transition.name === "add-report");

    // Upload the pdf to the file service
    const fileResult= await sdk.files.create(`measurement-${document.id}`, pdf);

    // Transition the document to analyzed
    await sdk.data.documents.transition(
        'blood-pressure-measurement',
        document.id,
        // Report property is added to the data to store the file service token
        { id: transition.id, data: { report: fileResult.tokens[0].token }}
    );
}

module.exports = {
  createPDF
}

Feel free to examine the code to see how we archieved this using the SDK.

Deploy everything

First we need to build the task. In 2-workflows do

npm run build-flow-2

And then to sync everything:

npx exh sync

Test the updated task

Use the scripts in examples to test your task as follows:

➞  node create-measurement.js                                                                                                    
Enter systolic value: 10
Enter diastolic value: 20
🎉 Created a new measurement document with id 6568537e5a8b65a7c7e3de77

➞  node get-measurement.js                                                                                                       
Enter document ID to retrieve: 6568537e5a8b65a7c7e3de77
Retrieved document 6568537e5a8b65a7c7e3de77
{
    "id": "6568537e5a8b65a7c7e3de77",
    "groupIds": [],
    "userIds": [
        "6310a263cff47e0008fb2221"
    ],
    "creatorId": "6310a263cff47e0008fb2221",
    "status": "report-available",
    "statusChangedTimestamp": "2023-11-30T09:19:07.591Z",
    "data": {
        "systolic": 108,
        "diastolic": 83,
        "timestamp": "2023-11-30T09:18:54.756Z",
        "category": "normal",
        "report": "6568538b657f242ca661d0a4-21254617-4848-4cb0-a83b-ee93ec9ef45d"
    },
    "updateTimestamp": "2023-11-30T09:19:07.598Z",
    "creationTimestamp": "2023-11-30T09:18:54.872Z"
}
➞  node get-file.js                                                                                                                 
Enter file token: 6568538b657f242ca661d0a4-21254617-4848-4cb0-a83b-ee93ec9ef45d
Retrieved file & stored as report.pdf

Go ahead and open the PDF file to see the report!

Test your task locally

Important: when testing locally, make sure that the analyze-blood-pressure task is not running in the Extra Horizon backend. Otherwise any created measurement will automatically be process by that task. You can easily remove the task using the CLI:

npx exh tasks delete --name=analyze-blood-pressure

In examples there's a test-local-task-flow-2.js script to run the task locally.

here
here