Toggle document index


This scenario involves these apps:

  1. Developer Career Site, a simple ATS that renders the opening details page with an "apply now" button embedded in it
  2. Simple Apply, an app that provides the "apply now" user interface, and pushes the job application into the ATS
  3. Tracker, an app that provides the TAS tracker functionality

In this scenario, a candidate:

  1. Visits the tenant's corporate careers site hosted by jobly to look at a specific opening
  2. Clicks apply now
  3. Is sent to the Simple Apply app
  4. Logs in using Google
  5. Completes a job application

Other scenarios that are also handled by Simple Apply, but not covered in this doc:

  1. Candidate "registers" interest (does not apply for any specific job), possibly for a given category (e.g. Marketing)
  2. Candidate views their "existing data" (e.g. their resume, profile) using Simple Apply, but within some larger container that lets also navigate to:
    • their in progress job applications
    • their draft job applications (held by Simple Apply)
  3. Candidate applies for a shopping cart of jobs in one operation
  4. candidate clicks on a job they have already have a draft for
  5. candidate clicks to apply for a job that has a gating job

Jobly (ATS) renders the job details page

Create a tracker

In this scenario, jobly, the ATS, has its own particular system for candidate source tracking which involves passing both candidateSource and deviceType parameters.

In this example, the candidate has reached the job by clicking on the link below (within a Facebook post), and the ATS has detected that the candidate is using a mobile device:

The TAS tenant APIs use a different convention for source tracking, called tracker.

If jobly was tracker-aware, the URL would have looked more like this (and jobly would not have needed to create the campaign tracker:

However the ATS is not tracker-aware, so it needs to create a campaign tracker that will embed candidateSource and deviceType.

POST /trackers/root
Response: 201 Location: 763019932

Embed the apply now button

With the tracker set up, the ATS (jobly) is ready to render the job details page.

As part of that, the ATS asks all interested apps whether they have a candidate-facing button (e.g. an apply button, join talent pool button, social sharing tool, view counter, etc.) that they would like to inject into the job details page.

Since the candidate is not logged in, jobly calls the not-on-behalf version of the API to do this:



Since the candidate is not logged in, the apply button(s) can't say (for example), "You've already applied". Instead, the panel shows whatever is appropriate for a completely unknown candidate, which is to allow apply.

If the candidate was logged in, jobly would use the on-behalf version in stead:



Candidate clicks apply

The entire job details page, and the apply button within it, are now rendered, so the candidate sees the "apply now" button at the bottom of the job description.

Note how the apps that generated the individual button links derived the loggedIn query parameter value from whether the button was generated from an on-behalf API call (logged in candidate) or for an anonymous candidate.

The candidate clicks on the Simple Apply link and is directed to the apply form.

Apply form

Candidate is now at:

Simple Apply renders its apply form slightly differently depending on whether a candidate is logged in or not. An anonymous visitor can't complete the form but can only view it. An anonymous visitor also sees the login disco panel embedded at the top of the page.

Simple Apply has the loggedIn filter described in conventions.html, but the filter doesn't fire since loggedIn = false.

The page is white listed in the proxy's SSO settings.

Simple Apply checks for the tazzy-saml header. Even though this page is white listed, its possible the candidate did log in previously somewhere else on Simple Apply. In this example, the header is not present.

Render for non-logged in candidate

Simple Apply's goal is to show as much as possible of the apply "form" to the candidate, so that they can assess how much work it will be, and hopefully be enticed by Simple Apply's support for cloud storage (dropbox etc.) for resumes, mobile-friendly UI, ease of login via social networks, etc.

At the top of this "not logged in" page is an iframe displaying a panel of login controls courtesy of the TAS identity system as per conventions.html.

Simple Apply fetches edit spec for non-logged in candidate

In this example the candidate is not yet logged in, Simple Apply can't check for role==internal on the TAS session - instead it can only show those questions that an external candidate would need to answer. If the job is internal only, Simple Apply would show no details at all, and would only offer the candidate a way to log in (so they could prove they were internal).

Deciding what to display to the candidate may involve complex logic, which does not belong inside Simple Apply, but is more likely in the ATS.

Simple Apply asks for an edit spec for applying to this job by an anonymous candidate.

POST /editSpecs/fetches/apply/7622/anonymous


The response includes all the information needed to render the apply form to an anonymous candidate (we look in more detail later).

We want them to see how easy the process was, but so that no-one can actually apply until they are logged in, Simple Apply renders the form with all fields disabled.

Candidate logs in to Simple Apply

The candidate sees the apply form, and decides that they are prepared to invest their time in applying, so logs in using Google (within the iframe-ed panel).

TAS redirects the user (still inside the iframe) back to:

That resource is now served up, since SSO now allows the request through. It is not a web page but instead responds with a http 302 redirect. The user is redirected to the logged in page, i.e.:

The redirect happens to the outer page, not the iframe - so now the user's browser address bar has the above in it, and the candidate is looking at the logged in page.

Now the candidate has logged in, their roles are available (e.g. role == internal).

Simple Apply re-renders its page, now knowing who the candidate is.

Simple Apply renders its page (candidate logged in)

Re-fetch the edit spec

Absorb the candidate's existing information into the intent

Now that the candidate has logged in, Simple Apply is ready to re-fetch the application form, this time for real (e.g. maybe now jobly knows who the candidate is, it can detect they've been black-listed and prevent the apply altogether).

Design decision - replace vs. merge of the candidate's categories

The POST /candidates/me API, which Simple Apply will eventually use to push the edit into the jobly ATS, supports two modes for setting the candidate's categories.

The category values in the request are merged with any existing values on the candidate. Suitable for one-click or offline scenarios, e.g. "enter your email here to hear about new finance jobs", where the UI never had a chance to query the candidate's existing data, and does not want to overwrite it.
The category values in the request completely replace any existing values on the candidate. Suitable for online scenarios, like a conventional apply form, where the candidate logs in and the UI has a chance to present all of their new and existing categories to them for tailoring.

Simple Apply uses replace semantics. Read the rest of this section for more details or skip on.

TODO: make clearer.

At the moment, the intent contains no categories, just the job that the candidate wants to apply to. When we get the edit spec back from jobly we might find some categories in there that were implied by the job for example the job "Co-pilot long-haul, Pacific" might imply expertise == pilot, which might in turn unlock some data entry fields to do with being a pilot.

However Simple Apply now wants to merge in any existing category values that the candidate already has (assuming they have a row in the ATS).

For example the candidate might have previously profiled themselves as expertise == CEO.

Simple Apply's philosophy is to encourage candidates to keep their profiles accurate, so in this case it wants to add CEO to the intent.

This may result in the apply form containing information relevant to CEO, possibly confusing the candidate who thought they were applying to be a pilot, and has forgot that they had partially profiled themselves as CEO. On the other hand, it may encourage them to unprofile themselves as CEO, keeping the database cleaner.

In summary, Simple Apply uses the philosophy of presenting to the candidate their entire category selections, allowing them to make changes, and then completely overwriting (with semantics == replace) the categories on their candidate record.

Another philosophy would be for Simple Apply to only present the categories related to the job the candidate was applying to, ignoring any other stuff lurking on their candidate record, and then use semantics == merge when applying the changes to the candidate record (via POST /candidates). The advantage to this is that the candidate only has to provide valid data for the job they are applying to, and does not have to clean up their record as a whole.

Fetching the candidate's existing information into the intent

Since Simple Apply is going to use replace semantics for the candidate's categories, it now needs to create a merged set of the candidate's existing category values and the category values indicated in the edit spec.

POST /candidates/me/categories/merges


If the API returns 404, the candidate does not already exist. Simple Apply just uses the values from the edit spec in this case.


  1. The candidate has logged in
  2. We've created an intent which indicates
    • what categories they want to end up with, i.e. the sum of what they already had and any new ones
    • the job they want to apply for

Simple Apply again fetches the edit spec, this time using the new intent, and the on-behalf API

POST /editSpecs/fetches/apply/7622/me


How the ATS builds the edit spec

To build the edit spec, the ATS (jobly) will use data from:

  • the sub field of the OAuth token passed to the edit spec fetch. This provides access to the candidate's role(s), e.g. whether they are internal
  • the ATS database - e.g. whether the candidate has been made redundant within the last 2 years, or failed a drug test during a previous recruitment process, and hence cannot apply at all

Note that the amended intent may indicate that, even though the candidate wishes to apply for some given job, the ATS may have other plans for them and may have amended their intent(in this case the message should explain to the candidate what and why). For example:

  • candidate applying for pilot jobs must also select a category value for pilot
  • candidates applying to some specific customer service job must first apply to a customer service talent pool role

Fetch the required engagements


Great! Now Simple Apply has the details it needs to begin building its UI.

Set up UI

Simple Apply creates the UI that it will display to the candidate.

To populate the UI, Simple Apply will merge data in from:

Simple Apply uses a merge strategy that prevents an old draft from overwriting candidate details. E.g. candidate starts to apply for A, saves as a draft with empty personal details, then moves on and applies to B with completed personal details, then returns to apply for A. We don't want the empty personal details on the draft for A to overwrite the filled in details on the candidate record.

Display the message

At the top of the page, Simple Apply displays the message (if any) in the style suggested. In a situation where the job is internal only, and the candidate is not internal, there might be nothing displayed but the message. In other situations there might be no message at all.

Set up input fields and controls

Simple Apply now uses the information from the candidateModel and applicationModel sections to set up the UI - i.e. when applying for a job, these are more or less the application form.


Simple Apply gets existing vcard values:

GET /candidates/me/vcard


Populate default values

The inputModel also includes some default values, e.g. for individual fields in the items section(s). These should be loaded into the input controls.

Populate category controls

Since categories may be large (e.g. a definitive list of every county in the US) it's up to Simple Apply to decide how to fetch all possible values so the user can select. For small categories, Simple Apply might fetch the entire category. For larger ones, it might use ajax calls to only fetch values as, say, the user clicks to expand a node in the hierarchy.

Get list of all categories ..
GET /categories

.. and its tree of nodes
GET /categories/10033/values

Fetch previously entered data

Once the UI has been laid out, Simple Apply next needs to load the fields with any existing data (this only applies when there is an existing candidate record). Specifically:

The classes of items as per above are: personal details (vcard) job alert (notifications) flag categories etc..

Load existing resume

Simple Apply fetches details of the candidate's existing resume (not the stream itself which might be enormous) TODO

GET /candidates/me/resume/meta


Simple Apply also displays a link which the candidate can click through on to open their existing resume document. TODO

GET /candidates/me/resume/asStream

Load details of existing applications

If the candidate already has a record in the database, Simple Apply now loads up any previously entered candidate data for the job application(s).

Normally candidates are applying from scratch, so there is no previous data, and then once they have applied, they cannot go back and change any application data.

But its possible that the candidate has previously submitted this application. This could happen if:

GET /applications/byCandidate/{candidateID}/byOpening/{openingID}


GET /applications/902533/items

Using data from the TAS session

TODO: clarify that Simple Apply can access name, image, email off of the SAML assertion using the new helper API. Pre-populate fields in the UberApply form

Candidate fills out apply form

Simple Apply now renders the page to the candidate, who can start editing, selecting and generally completing the form.

Candidate uploads a new resume

The candidate wants to replace their existing resume, so uploads a new document which Simple Apply stores somewhere, perhaps in a temporary file.

Save draft

Simple Apply includes split-level draft functionality. This prevents candidate details being overwritten.

A single draft is held for candidate details, keyed by E/N
A single draft per job being applied for, keyed by E/N/J
On successful submission
   Of apply
      Candidate and job level drafts are deleted
   Of register
      Candidate level draft is deleted

The ATS (jobly) is unaware of any draft functionality, Simple Apply only pushes completed edits to it.

Candidate edits categories

While the candidate is using Simple Apply, they may change their categories.

If they do, then Simple Apply must re-fetch the edit spec, since a change in categories may result in knock on changes in the amended intent, and in the input model (e.g. a candidate who adds expertise == pilot must now answer additional questions).

This may be quite involved for Simple Apply, as it needs to temporarily preserve any data from the fields (perhaps using its own draft functionality), rebuild the UI using the new edit spec, then reapply that data.

More candidate interactions with the apply form

TODO: flesh out 
candidate clicks to view an existing (exists back on their candidate record) document attachment/CV 

candidate clicks to view a newly added (only present in draft) document attachment/CV
candidate deletes an existing answer (a no longer required one? any one?)

candidate deletes an existing document attachment/CV

candidate deletes a newly added document attachment/CV

candidate completes their application (draft gets deleted)

candidate deletes a draft application

candidate deletes their entire profile (maybe another app?)

Candidate submits application

Render the "Next steps" page

We need to show the candidate the engagements they are going to have to complete. This means first actually instantiating them if they haven't been already.

Eager creating - this creates the engagements even if the candidate just popped in to see how arduous it will be and browses away - in that case these will get cleaned up by the engagement hub's reaper. The reaper daemon reaps unfinalized engagements after 6 weeks (leaves time for candidate to return to a draft application from earlier).

POST /candidates/me/engagements/forJobs/instantiates
-- intent
[ "jobs": .. ]

    "engagement": 10022,
    "linkages": [ 13005, 13006 ]

Render the engagement's name

GET /candidates/me/engagements/{e}/candidate

Render each engagement into the page, passing the current page in as the relay page. Rather than iframeing, which would stop engagements rendering outside their area (e.g. large pop-up menus), we inject Javascript. More security risk. Better usability.

GET /candidates/me/engagements/{e}/candidateView?size=medium&relayPage=[this page]


Once the page is served, candidate sees panels for each engagement, along with tick or cross if the engagement has both "isPassFail" and "candidateSeesFail" true.

GET /candidates/me/engagements/{e}/engagementType

ATS tells engagement hub to finalize the engagement linkages that need to be completed by this candidate given the job(s) they are applying for.

The hub will bail if any one of the engagements is not complete.

POST /candidates/me/engagements/forJobs/finalizes
POST /candidates/me/engagements/forTalentPool/{tp}/finalizes

Finalize engagements


Attach the engamenents to their job applications or talent pool memberships.

The ATS handles this under POST /candidates.

Most of the work is delegated to the engagement hub.

After this all the engagements are finalized (hold application ID).

Job board tells .. present 
Job board tells .. prepare 

ATS tells engagement hub to finalize the engagement linkages that need to be completed by this candidate given the job(s) they are applying for.

POST /candidates/me/engagements/forJobs/finalizes
POST /candidates/me/engagements/forTalentPool/{tp}/finalizes
[ 12, 14 ]

Each of these can be mapped back to the underlying engagement.  

The result is an ordered list of engagement.
engagement hub creates a bunch of engagements as a result of this list.

The results are a set of engagement types.

An engagement type may be tailored to a specific job. In that case the 

Those types can make use of the intent to

Once the candidate has finally completed their work and pressed submit, the completed edit (i.e., job application in this example]) is sent to the ATS (jobly).

POST /candidates


ATS processes the event

TODO: clean up any existing messages.

The ATS processes the event. This may result in creating a new candidate or updating an existing one.

Processing the tracker

The ATS fetches the chain of trackers, starting from the one passed across to POST /candidates.

GET /trackers/763019932/chain


Now the ATS applies its custom logic to cope with its own source conventions.

In the case of jobly, it wants two pieces of metadata about the application:

  1. candidateSource
  2. deviceClass

The ATS first looks to see if these were passed in, and if not it establishes them from the information in the trackers.

TODO: cleanup

// see if candidateSource was passed in
candidateSource = destinationTracker.meta.candidateSource;
if (candidateSource == null)
   // source was not passed in, so derive it from the visit tracker's fields
   candidateSource = extractCandidateSource(visitTracker.landingURI);
// likewise for device class
deviceClass = destinationTracker.meta.deviceClass;
if (deviceClass == null)
   // deviceClass was not passed in, so derive it from the user agent
   // this ATS might use a user agent database to resolve down to mobile == true or false for example
   deviceClass = extractDeviceClass(visitTracker.userAgent)

Candidate is redirected back to job details page

Based on the http result from the event post, Simple Apply then displays the "Successful" page.

A button on that page links the user back to the relay page (i.e the original job details page in this example of applying for a job).

User is now back at the job details page.

TODO: The recruiter views the underway/ completed/ finalized/ engagements for a candidate.