> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appsignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mission Control Jobs: a job dashboard inside Rails

> Mount the Mission Control Jobs engine to browse queues, workers, and failed jobs from inside your Rails application, and restrict the route to signed-in administrators.

export const YouTube = ({id, title, description, presenter, duration, republished}) => {
  const playlistId = "PLFHQSOKTqHXA";
  const playlistUrl = `https://www.youtube.com/playlist?list=${playlistId}`;
  if (!id || id === "YOUTUBE_ID") {
    return <div style={{
      border: "1px dashed currentColor",
      borderRadius: "0.5rem",
      opacity: 0.7,
      padding: "2rem 1.5rem",
      textAlign: "center",
      fontSize: "0.875rem"
    }}>
        <strong>Video not published yet.</strong>
        <br />
        {title ? `"${title}" has no YouTube id.` : "This tutorial has no YouTube id."}{" "}
        Replace <code>YOUTUBE_ID</code> in this page with the id from the
        video's URL, once it is in the{" "}
        <a href={playlistUrl}>GoRails x AppSignal playlist</a>.
      </div>;
  }
  const prettyDate = republished ? new Date(`${republished}T00:00:00Z`).toLocaleDateString("en-US", {
    year: "numeric",
    month: "long",
    day: "numeric",
    timeZone: "UTC"
  }) : null;
  const schema = {
    "@context": "https://schema.org",
    "@type": "VideoObject",
    name: title,
    embedUrl: `https://www.youtube-nocookie.com/embed/${id}`,
    contentUrl: `https://www.youtube.com/watch?v=${id}`,
    thumbnailUrl: [`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`],
    creator: {
      "@type": "Organization",
      name: "GoRails",
      url: "https://gorails.com"
    },
    publisher: {
      "@type": "Organization",
      name: "AppSignal",
      url: "https://appsignal.com"
    }
  };
  if (description) schema.description = description;
  if (duration) schema.duration = duration;
  if (republished) schema.uploadDate = republished;
  if (presenter) schema.author = {
    "@type": "Person",
    name: presenter
  };
  return <div style={{
    marginBottom: "1.5rem"
  }}>
      <iframe width="100%" height="450" src={`https://www.youtube-nocookie.com/embed/${id}?list=${playlistId}`} title={title} style={{
    borderRadius: "0.5rem",
    border: 0,
    display: "block"
  }} allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen />
      <p style={{
    fontSize: "0.875rem",
    marginTop: "0.5rem"
  }}>
        <a href={`https://www.youtube.com/watch?v=${id}&list=${playlistId}`}>{`Watch "${title}" on YouTube`}</a>
      </p>
      <script type="application/ld+json" dangerouslySetInnerHTML={{
    __html: JSON.stringify(schema)
  }} />
    </div>;
};

<YouTube id="HfJp_leekA8" title="Inspect background jobs with Mission Control" description="Mount the Mission Control Jobs engine to browse queues, workers, and failed jobs from inside your Rails application, and restrict the route to signed-in administrators." presenter="Chris Oliver" duration="PT9M43S" republished="2026-09-19" />

Presented by Chris Oliver for [GoRails](https://gorails.com). Republished on AppSignal September 19, 2026.

Solid Queue keeps its state in your database, which means you can query it from the Rails console. Mission Control Jobs puts a user interface in front of that state, mounted inside your own application. You can browse queues, workers, and failures without writing queries.

It talks to Active Job rather than to one specific backend, so it reads Solid Queue and Resque today and is built to accommodate others. AppSignal instruments the same jobs through its [Active Job](/ruby/integrations/active-job), [Solid Queue](/ruby/integrations/solidqueue), and [Resque](/ruby/integrations/resque) integrations.

<h2 id="what-this-covers">
  What this covers
</h2>

* Mounting the engine and restricting who reaches it.
* Reading queues, workers, and job arguments.
* Diagnosing a failed job from its recorded error and backtrace.

<h2 id="requirements">
  Requirements
</h2>

| What               | Value                  |
| ------------------ | ---------------------- |
| Gem                | `mission_control-jobs` |
| Active Job backend | Solid Queue or Resque  |

<h2 id="install-and-mount-the-engine">
  Install and mount the engine
</h2>

```bash theme={null}
bundle add mission_control-jobs
```

Then mount it in `config/routes.rb`:

```ruby theme={null}
mount MissionControl::Jobs::Engine, at: "/jobs"
```

<h2 id="restrict-the-route">
  Restrict the route
</h2>

The jobs interface exposes job arguments, which routinely contain record IDs, email addresses, and anything else you passed to a job. Do not mount it unauthenticated.

With Devise, wrap the mount in a route-level constraint:

```ruby theme={null}
authenticate :user, ->(user) { user.admin? } do
  mount MissionControl::Jobs::Engine, at: "/jobs"
end
```

A request from anyone who fails the constraint gets a `404` rather than a sign-in prompt, so the route does not advertise that it exists. If your authentication library has no routing helper of this kind, Mission Control also accepts controller-level configuration.

<h2 id="read-a-job">
  Read a job
</h2>

The queue list shows each queue and the jobs on it, and the worker list shows the connected dispatchers and workers with their last heartbeat. Opening a job shows its class, queue, arguments, and the times it was enqueued and finished.

Arguments are worth understanding, because they are stored as JSON rather than as Ruby objects. An Active Record instance passed to a job appears as a Global ID reference. A hash with symbol keys carries a marker recording that its keys should become symbols again when the job is deserialized. What you see is the serialized form, not the objects your job receives.

<h2 id="diagnose-a-failed-job">
  Diagnose a failed job
</h2>

A failed job keeps its error class, message, and backtrace. That is normally enough to identify the line that raised, fix it, and retry the job from the same screen.

<h2 id="where-appsignal-fits">
  Where AppSignal fits
</h2>

Mission Control answers "what is in my queues right now". It is a live view of the backend's own tables, and it is scoped to one application.

AppSignal answers the questions that need history and aggregation across deploys:

* **Is this failure new, and how often does it happen?** [Error tracking](/errors) groups job failures into incidents, with a count and a first-seen time, rather than a list of individual failed rows.
* **Which jobs got slower, and when?** Jobs are reported in the `background` [namespace](/guides/namespaces) with their own [performance traces](/performance-tracing). Pair that with [deploy markers](/guides/deploy-markers) to line a regression up against the deploy that caused it.
* **Did the job run at all?** A job that is never enqueued leaves nothing behind in either tool. [Check-ins](/check-ins) report on schedule adherence, and tell you about the run that did not happen.

Run both. Mission Control is the operator's console; AppSignal is the record of what has been happening.

<h2 id="transcript">
  Transcript
</h2>

Transcribed from the video and lightly edited: the automatic captions misheard a number of product and API names, and those have been corrected.

<Accordion title="Read the transcript">
  **0:02** Hey there, I'm super excited about this episode. Mission Control is finally publicly available. It has some bugs, but they will get fixed very quickly, I'm sure, but Mission Control Jobs is the first version of Mission Control that DHH announced at Rails World 2023. So what's interesting is this is called Mission Control Dash Jobs, meaning that we'll probably see other Mission Control features in the future. So the way to set this up is you add Mission Control Jobs to your gem file, and then you add this line to your routes file to get started.

  **0:41** So we're gonna do that, we're gonna say bundle add, Mission underscore control dash jobs, and you will run that, and then I'm gonna run the Rails server, which I have a Rails application here that we created in the Solid Queue episode that has Solid Queue installed, and it is set up as the Puma plugin to run it automatically. So if we open up Puma.rb, you'll see here the Solid Queue plugin, this is going to run Solid Queue whenever Puma starts up and shuts down, it will automatically shut down Solid Queue for us as well. So that is all you need to do to set up Mission Control Jobs. So here, if we run our Rails application, we can then go to our routes file, and we can add that line here to mount the Mission Control engine. We can also, if you were using Devise, use authenticated user do around this in order to check and see if the user is logged in and then allow this route to be accessed.

  **1:45** Otherwise, if a user is not logged in, it will just 404. So this is a handy way of adding authentication to the Mission Control route, but you can also add authentication to the controller with some configuration if you're using something else that doesn't have these routing helpers. But I find these really useful because I don't have to go add any configuration for it, it's just done here in my routes file. So once that is added, we do not have Devise on here, so we're just going to leave those commented out. But now we can access the jobs route.

  **2:22** You can see I've been playing with this a little bit. We have the default queue, which is going to be, of course, created by default if you haven't specified the exact queues you want. We have some jobs, I was playing with this a month ago with Solid Queue, but we've also got the jobs I was playing with a little bit ago. So we can see here the workers that are connected. I have my one worker that is set up by Puma and running here, it is connected.

  **2:52** Last heartbeat was one minute ago. We can refresh and see if that updates or anything. But this is Mission Control Jobs. So if we were to run a job in our Rails console or something, we can say Rails console and we can open up, say, I have some examples here, but if we want to say, let's send a mailer with a post in it. We can do that.

  **3:21** It will in queue the job, mail delivery job. We get all that information there and then we can refresh our UI. We have an in-progress job, that Action Mailer, mail delivery job. Running by worker 26 less than 20 seconds ago. If we refresh this, it should be done and it was successful and we have that here under finished jobs, which is awesome.

  **3:43** We can click on the jobs and see the arguments, the job ID, which queue it was, when it was in queueed, when it was finished, and we can see all the raw data for it. So this is handy to see your arguments so we can tell what job class it was, what was the ID, any of our arguments. So we have the user mailer, the receipt method, we have deliver now, we have params. So this is converted the post instance into a global ID. It also knows that there are symbols that we want for our hash keys and because we have to convert this to JSON to store in the database, this has a special representation for those and it says, hey, inside params, there is a post key, but we want to convert that to a symbol when we turn it back into a Ruby hash.

  **4:34** So this is all super useful information and I wanted to point out that if we were to make a mistake in our user mailer, like we reference a variable that does not exist, and we go back to our terminal and try it again, we might want to reload. So we have, or I guess the job probably is going to be queued with the bad logic. There we go, we have a new fail job. Less than a minute ago, this active delivery, or mail delivery job has failed. And what's nice is it keeps track of the error information, so the type, the message, and the back trace.

  **5:14** So we can see here, user mailer line 11 in the receipt method called an undefined local variable or method ASDF, which is correct, and we can then fix that and then retry our job. Now, unfortunately, the current version of mission control has a bug in it, and so retry does not actually work. That will be fixed very shortly. I reported that already and we'll probably make a pull request for it as well, but that will be fixed very soon. Other things to look at here are, there's not a whole lot, but if you type jobs help in your Rails console, you will see this output here that says, you can connect to a job server with connect to app ID and server ID.

  **6:00** And we'll print out examples down here. What's missing, I made a pull request for this already is the double quotes around this. I was a little confused by that. And they don't have to have the app ID and colon on this or the server ID, I guess. This is probably the app ID, but you can say connect to double quotes and give it a string and use one of the available job servers that it lists out here that will list all of the job servers that are connected to your database from what I can tell.

  **6:32** And then it will have you connected to that where you can then say Active Job.jobs and you can see jobs loaded from the database. Now we don't have any jobs running or whatever, so this is basically giving us nothing, but you will be able to access those and there's a whole bunch of stuff that you can call to view those jobs. So this is kind of the interface that it uses to grab that for the UI because this actually supports rescue, which stores things in Redis, but Solid Queue stores them in your database. So it has a flexible backend to talk to those different services. So you can imagine that eventually we will probably have one for integration for Sidekiq and good job and Q and all the other ones out there.

  **7:23** They will just need to interface with this and that will be pretty much it. So this is cool, you can access the failed jobs, so if we want to do that and pop that in here and see those and then basically query those pending jobs for a specific queue and so on. So it's pretty awesome. Lots of things that you might want to do with those, but in general, you probably won't mess with that much. The UI is really the bread and butter here of Mission Control jobs.

  **7:57** Yeah, so I'm kind of curious if we were to open up the Rails console and just call Active Job jobs, what happens? I guess that method is defined when you connect to one of the job servers. So that is probably why, or if we say jobs help. Yeah, so in a defined method, that must be defined dynamically when we connect. So that's pretty cool.

  **8:28** I don't know if there's a disconnect method. I would assume there is, but it may just not be documented or whatever, but that's pretty cool. A little handy helper there for accessing your job servers and getting into the weeds a little bit. So that is Mission Control jobs. I assume that's a hint that there will be much more cool things for Mission Control in the future, but this is what was announced and it works great.

  **8:53** I love using Solid Queue to store my jobs in the backend, but I don't even have to worry about managing another process because of the Puma plugin. This is gonna be a great way to run jobs in the future in your applications without a separate service since it just takes advantage of the database you're already using. So that is it for this episode. There's really not a lot to it. This is it.

  **9:21** You have access to talk to the Active Job jobs if you want, but that is about it. It's a great little UI interface. You can add to your Rails applications today. So that's it. If you have any questions, let us know in the comments below and we will talk to you in the next one.

  **9:37** Peace.
</Accordion>

<h2 id="related-tutorials">
  Related tutorials
</h2>

* [Solid Queue](/tutorials/ruby/solid-queue)
* [Batching background jobs](/tutorials/ruby/batching-background-jobs)

<h2 id="about-this-tutorial">
  About this tutorial
</h2>

This tutorial summarizes a GoRails screencast on Mission Control Jobs, by Chris Oliver. The screencast is the original work. GoRails publishes it, and the rest of the series, at [gorails.com](https://gorails.com).
