Skip to content
Latchkey LogoLatchkey home

GitHub Actions step cannot have both uses and run, and what you get

A GitHub Actions step cannot have both uses and run, and that sentence was once the message you got, but the parser that ships today does not contain it anywhere. What it produces instead names one of the two keys as unexpected, which is a different sentence and a more precise one once you know how the reader arrives at it.

A step narrowing from two candidate shapes to one as each key is read, and where the error lands
The reader narrows as it reads. The first of the two keys chooses a shape, and the second has nowhere to go inside it.

What this error means

The workflow stops running and the commit carries an invalid file annotation with a line, a column and a quoted key. The quoted key is one of the two you wrote, and which one it is depends on the order you wrote them in rather than on which one is wrong. If you came here from an older answer expecting a sentence about both keys, the thing to check is whether your annotation says that at all, because a message that quotes a single key is telling you something the older sentence did not.

Reconstructed from the key branch of TemplateReader and the UnexpectedValue format string in actions/runner
Invalid workflow file: .github/workflows/ci.yml#L9
(Line: 9, Col: 9): Unexpected value 'run'

What the reader does with the second key

In the published schema an item in a step sequence is a choice between two mapping shapes. One requires the run key and additionally allows a working directory and a shell. The other requires the uses key and additionally allows with. Everything else the shapes accept, env among it, is on both. Neither shape lists the other required key, and neither permits loose keys.

The reader does not collect your keys and judge them at the end. It narrows while it reads. Each key is looked up across the shapes still in play, and when a key exists in some of them and not others, the ones without it are removed from the list. Reading uses therefore deletes the run shape immediately, and reading run deletes the uses shape.

Whichever of the two you wrote first wins that race. The second key is then looked up against a single surviving shape which does not have it, there are no loose keys to absorb it, and the reader records an error naming the key. The format string it uses is the generic one for a key with no home, which is why the message says nothing about steps, actions or commands.

That message belongs to a page of its own, since it is raised from several places for several findings. Our page on the unexpected value annotation is where to read about the file, line and column prefix and how to tell a rejected key from a rejected value. This page is only the branch where a step was narrowed to one shape and then given a key from the other.

What the step containsShapes still in play at the endWhat the reader records
uses then runthe uses shape onlyunexpected value naming run
run then usesthe run shape onlyunexpected value naming uses
Neither keyboth shapesa message asking for one of the missing properties
uses with with and envthe uses shape onlynothing, this is valid

Common causes

Indentation merged two steps into one item

The commonest cause and the hardest to see. A missing dash turns what should be a new sequence item into another key of the previous one, and the file stays valid YAML throughout.

A command was replaced by an action in place

Somebody swapped the middle of a step, putting an action reference where a command was, and left the other key behind. The diff touches one line, which is why review passes over it.

An example was pasted inside an existing step

Documentation examples begin with a dash and assume they are being added as a new item. Pasted into the body of an existing step they become extra keys of it.

The step was meant to do two things

Occasionally the intent really was to run an action and then a command. In our experience this is rarer than the accidental cases, and the answer is the same: two steps.

How to fix it

Read which key the annotation names

  1. Open the annotation and note the quoted key and the column.
  2. The quoted key is the second one in your file, not necessarily the wrong one.
  3. Go to that line and look at whether it should have started a new sequence item.

Split into two steps

Give each of the two a dash of its own at the same indentation as the other items in the sequence. Name them if the job has several steps, since the default name of a run step is the command text.

.github/workflows/ci.yml
steps:
  - name: Check out
    uses: actions/checkout@v5
  - name: Install
    run: npm ci

Check the whole sequence, not just the reported line

An indentation slip that merged one pair often merged another further down, and the reader stops recording after it has enough to reject the file. Read every item in the sequence once before pushing again.

Validate before pushing

The same schema is available to editor tooling, so a file that will be rejected can be rejected locally. This is the only one of these fixes that prevents the next occurrence rather than resolving this one.

The sentence this page is named for

The wording about a step not being able to have both keys is real in the sense that people met it, and it is old. We looked for it as an exact phrase, which is safe to do here because it carries no numeral and so is not damaged the way a phrase containing a number would be. The reports we fetched and searched that genuinely contain it cluster in the period before 2021, and the modern results we checked mostly do not contain the phrase in their bodies at all.

It is also absent from the sources you can read. The message catalog that the template reader draws from lists the generic messages by name, and there is no entry for anything about uses and run. Neither the runner nor the language services parser contains the sentence, and a code search across GitHub turns it up only in third party linters that wrote their own wording, and in this hub own retired corpus.

The honest summary is therefore in two parts. A validation component GitHub does not publish once produced that sentence, and we cannot say what that component does today because we cannot read it. The parser that is published produces the message quoted above, from the branch described above. If your annotation carries the older sentence, you have learned something about GitHub that we could not establish from the outside, and the fix is identical either way.

Splitting the step, and the indentation that merged it

The fix is always the same: one step does one thing. Put the action in its own item with uses and the command in its own item with run. Nothing is lost by the split, because the two were never going to run as one thing anyway.

The more useful question is how they got merged, and the answer is usually indentation rather than intent. In a sequence, a new item starts with a dash. A line that should have begun a new item and instead sits at the same indentation as the keys of the previous one becomes another key of that item. The diff looks tiny and the YAML stays valid, which is why this survives review more often than a mistake this simple should.

The version that catches experienced people is a partial edit. Replacing the command in a step with an action, and leaving the run line above or below it, produces exactly this. So does pasting an example from documentation into the middle of an existing step rather than after it.

.github/workflows/ci.yml
# merged: run became a second key of the first item
steps:
  - uses: actions/checkout@v5
    run: npm ci

# split: two items, each a step
steps:
  - uses: actions/checkout@v5
  - run: npm ci

Why there is no recorded run on this page

The file does not compile, so no run is created, no job is queued and no runner is involved. There is no log to capture because there was never a process. What exists is an annotation on a commit, and its text is a format string in the published parser, which is quoted here rather than photographed.

It is worth being explicit that the block above is reconstructed rather than copied from somebody run. The position in it is illustrative and the message text is assembled from the generic format string with the key that the branch would pass to it. Saying so matters on a page whose whole subject is that a remembered message and the current one are different sentences.

There is nothing to repair. A step that cannot be narrowed to one shape is an ambiguity in your file, and no runner can guess which half you meant.

How to prevent it

  • Keep one action or one command per step, and treat that as a review rule.
  • Watch for a missing dash whenever a step gains a key it did not have.
  • Paste documentation examples after a step, never inside one.
  • Validate workflow files in the editor against the published schema.

Frequently asked questions

Why does the annotation name only one key?
Because the reader narrows as it reads. The first of the two keys chooses which step shape applies, and the second is then looked up against that single shape, found absent, and quoted. Which key gets named depends on the order you wrote them in.
Is the message about both keys still produced?
Not by anything published. The sentence appears in reports clustered before 2021 and is absent from the runner and the language services parser, and from the message catalog the reader draws on. The component that produced it is not published, so we cannot say what it does today.
What happens if a step has neither uses nor run?
The reader reaches the end of the step with both shapes still in play and cannot choose. It records a message asking you to add one of the properties that would decide it, and lists the ones that appear in only one of the two shapes.
Can a step run an action and then a command?
No, and the split costs nothing. Put the action in one step and the command in the next. If they need to share state, write it to a file or to the step output rather than trying to fuse the two into one item.

Related guides

References

Two steps became one and no runner was assigned. Latchkey runs the fixed file at $0.0025/min. Start free → 30-day trial · No credit card